mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
288 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7dcb6edca | |||
| 14cf0eff1d | |||
| a00c80f9e1 | |||
| 12d878531c | |||
| 588ab7ebaa | |||
| dfbccd917c | |||
| 37010529bc | |||
| f07d04d129 | |||
| f12036c2da | |||
| 520e266411 | |||
| 7bd86762a7 | |||
| 451f98e1d1 | |||
| 60fe4a0800 | |||
| 1ef109c042 | |||
| 65786597cc | |||
| f884b29d0d | |||
| b3304bf05b | |||
| 28796c659e | |||
| 9da15b5d96 | |||
| 46dc6316f9 | |||
| 16e65c70ad | |||
| a6f9e68739 | |||
| f85c0f1cc0 | |||
| 4b0b6dc8c6 | |||
| aab5364252 | |||
| e2c2b012f1 | |||
| d779394748 | |||
| c59d56c655 | |||
| 3480a33fce | |||
| a26b2a0097 | |||
| 37eee7e727 | |||
| 9771f3309e | |||
| c39f0ae317 | |||
| 95dff74fc8 | |||
| ab3dc5f8fa | |||
| 53c523444e | |||
| 1f3e3045ad | |||
| 952622a965 | |||
| 70393b6de4 | |||
| e8e824a730 | |||
| 3d33f559b1 | |||
| aa7abe7581 | |||
| 589c198d2d | |||
| 5891c0d955 | |||
| d29017fac9 | |||
| 6a6c890700 | |||
| 599ba52750 | |||
| 2cbe125e0b | |||
| 0eddb8a615 | |||
| 16ecf60800 | |||
| eabc0aaaa8 | |||
| 374a920b61 | |||
| 6ba0c25a92 | |||
| 48f703f990 | |||
| f407dbb712 | |||
| 92e0f5fcf9 | |||
| d608a64cc5 | |||
| abb3466c31 | |||
| a3ee49d8a9 | |||
| f2016e7f63 | |||
| bade488bdf | |||
| b426c62423 | |||
| 4ce0e1bd38 | |||
| fe7ff6cbfe | |||
| e2064c820f | |||
| 318488e229 | |||
| e251a21810 | |||
| 05249c7b51 | |||
| d7d81384c2 | |||
| 659da3ebba | |||
| 244248f2b7 | |||
| ea1c4a327d | |||
| 5c5d1dc161 | |||
| cd8cb47866 | |||
| 2484df1f81 | |||
| a57fc25b54 | |||
| 2437675c04 | |||
| ec07269122 | |||
| 3ac69f2d06 | |||
| a29184a171 | |||
| e05939f1c9 | |||
| ab0bce58f8 | |||
| d6b0833209 | |||
| 9f0f22c036 | |||
| 72f8ecc521 | |||
| 394d3546bf | |||
| a9365296ae | |||
| 30c435fee6 | |||
| c7d63beba2 | |||
| 9f0002db40 | |||
| 86f33005b9 | |||
| bd9e020df9 | |||
| b03ea055b0 | |||
| ae7f7fa7e8 | |||
| bec84aeb7b | |||
| c7f0f90bed | |||
| c7cd4e02b8 | |||
| 3bb3d71093 | |||
| c2183bd814 | |||
| 037f056d0c | |||
| db239e7319 | |||
| 26ae1ca3c8 | |||
| 13b2e423ed | |||
| 47733ba25e | |||
| c95959e845 | |||
| 75752b681b | |||
| 6174c4588b | |||
| d875839e5e | |||
| 1f38deca1e | |||
| 5d229fd917 | |||
| 628176a2d6 | |||
| 0b62d45738 | |||
| cb6e5999e1 | |||
| a16532dbbb | |||
| 354a5bb8af | |||
| e0b1899e97 | |||
| d37ad42426 | |||
| 44e6d1a313 | |||
| 30197e7dfa | |||
| 654a326098 | |||
| 43446bc692 | |||
| e9f010111d | |||
| 8184bc457d | |||
| e759716ce7 | |||
| 2db00211f5 | |||
| eb2e4349e8 | |||
| 7d76ce2d24 | |||
| 66efd3f2bb | |||
| 2941825577 | |||
| d094e52bd1 | |||
| d6b03fbabf | |||
| e03f53144b | |||
| b342c7403c | |||
| 37210ff661 | |||
| 6db3cb2f1b | |||
| f27418044b | |||
| 1b02d169ad | |||
| 84833011ec | |||
| 5e5f2f4d8c | |||
| b9898a9fbe | |||
| 687682120c | |||
| a1a9f3813e | |||
| 7deb4340b4 | |||
| b5be8a6fa4 | |||
| a665d3245a | |||
| a48691fe78 | |||
| b723a1a426 | |||
| 619c12cc64 | |||
| bb29a0e194 | |||
| 86476e6248 | |||
| da6fac4133 | |||
| 47ad3238dd | |||
| 4f7165530c | |||
| a36fa07aba | |||
| e846943e66 | |||
| 652c82daa9 | |||
| a6760a6cbf | |||
| 204d55ecec | |||
| 1f4511ca6e | |||
| 110b2b4c80 | |||
| dc2c76738a | |||
| 5344a0bc6a | |||
| 3299a9bc15 | |||
| 34a876e4e7 | |||
| 971360d5e0 | |||
| 76badfa01b | |||
| 56aeb6be65 | |||
| a9aa48d7a1 | |||
| 638203f9f1 | |||
| 98e043d17d | |||
| 3ad4b732d4 | |||
| 9c1f5c33a8 | |||
| f75503bf3c | |||
| 2c38487a54 | |||
| ae0b261e56 | |||
| e653741885 | |||
| e5ae80ab86 | |||
| ba2edca352 | |||
| c8ffdae859 | |||
| 8fc8c17522 | |||
| 8d9aab5217 | |||
| 25f91fec10 | |||
| c7b2cce131 | |||
| 3e5c2959db | |||
| 37ec2d4830 | |||
| ecd101d485 | |||
| cf5be91d2d | |||
| 28685a5055 | |||
| e23d928829 | |||
| 159fcb44ce | |||
| 8e69f41833 | |||
| 2bf652c6fd | |||
| 521f5632d7 | |||
| f10247db90 | |||
| fbe357b1e6 | |||
| 00cd7c333c | |||
| 86b50eda15 | |||
| 652df22831 | |||
| 1776d3d335 | |||
| 33baad35b8 | |||
| 12dd91ada1 | |||
| b901d73d9b | |||
| 61a16538a1 | |||
| 9e1f4c2bca | |||
| f51be82729 | |||
| fd5d8260bb | |||
| f2c4fdb0ae | |||
| a4a84bb27e | |||
| 4ddc8d758b | |||
| 8068cdebb6 | |||
| 45236a1f7d | |||
| e0a55f9c0e | |||
| c1d8c5e960 | |||
| 7ae1d4668e | |||
| adb7d40084 | |||
| 541c63d737 | |||
| f1d6f0c0be | |||
| 958b6f1d24 | |||
| 6d1d91d9f9 | |||
| fc0ecf4185 | |||
| 349586007c | |||
| 569a7c3c9e | |||
| bec683b67d | |||
| 38d3c3cae5 | |||
| f2d765351d | |||
| 5d8f60873d | |||
| b474778cf1 | |||
| 65247b6f0f | |||
| 5f3cfe044f | |||
| 81fe0bd12b | |||
| bfeaa3418d | |||
| 9584adc38a | |||
| 0096f2613c | |||
| ac82c8b08b | |||
| af3a66d60e | |||
| 42e0540395 | |||
| 2ee90483bf | |||
| 2084a123c2 | |||
| b500e556bf | |||
| 1b0b6bf7a8 | |||
| 85eee4849f | |||
| 9b8a533435 | |||
| 5abb90b539 | |||
| 0c8bc56930 | |||
| fdab68aa08 | |||
| 5aaab967a3 | |||
| ccad59dfab | |||
| 8a3eade6d3 | |||
| 39847a70f2 | |||
| d1e338ee48 | |||
| b243602d1c | |||
| 960c5a7baf | |||
| aca3575b6d | |||
| 2b4815a9f5 | |||
| 729890d847 | |||
| 31f424d589 | |||
| cb4bd8d065 | |||
| 1abd22ef0a | |||
| a3bcf25fae | |||
| 20e36c5bf3 | |||
| 5693ba524b | |||
| 9c6fbfca18 | |||
| 925ec100d6 | |||
| 58b27a1ccf | |||
| 867360c8dd | |||
| cb97257815 | |||
| c43e07d6b7 | |||
| 9426b60308 | |||
| ff9621bbd5 | |||
| 7a0a618c0d | |||
| 0be1ed5284 | |||
| 50a0c44cee | |||
| 76391434c2 | |||
| e8841c0fb3 | |||
| aec1d8f653 | |||
| c7f343a3b4 | |||
| 7ecb6d3e88 | |||
| 88c25d2c67 | |||
| 38cd455e4d | |||
| ee5e447d44 | |||
| 4d30a71434 | |||
| 39b7aed24e | |||
| 4de1bc4bba | |||
| e426693330 | |||
| 0b5b9b8297 | |||
| 7a014b06ec | |||
| 171d4ac77c | |||
| 5e212a6bf3 |
@@ -4,15 +4,15 @@ jobs:
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.x]
|
||||
go-version: [1.22.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
on:
|
||||
pull_request: {}
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
name: Semgrep config
|
||||
jobs:
|
||||
semgrep:
|
||||
name: semgrep/ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||
SEMGREP_URL: https://cloudflare.semgrep.dev
|
||||
SEMGREP_APP_URL: https://cloudflare.semgrep.dev
|
||||
SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: semgrep ci
|
||||
@@ -17,3 +17,4 @@ cscope.*
|
||||
ssh_server_tests/.env
|
||||
/.cover
|
||||
built_artifacts/
|
||||
component-tests/.venv
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# !/usr/bin/env bash
|
||||
|
||||
cd /tmp
|
||||
git clone -q https://github.com/cloudflare/go
|
||||
cd go/src
|
||||
# https://github.com/cloudflare/go/tree/f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38 is version go1.22.5-devel-cf
|
||||
git checkout -q f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38
|
||||
./make.bash
|
||||
+29
-18
@@ -7,13 +7,17 @@ if [[ "$(uname)" != "Darwin" ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "amd64" != "${TARGET_ARCH}" && "arm64" != "${TARGET_ARCH}" ]]
|
||||
then
|
||||
echo "TARGET_ARCH must be amd64 or arm64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go version
|
||||
export GO111MODULE=on
|
||||
|
||||
# build 'cloudflared-darwin-amd64.tgz'
|
||||
mkdir -p artifacts
|
||||
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
PKGNAME="$(pwd)/artifacts/cloudflared-amd64.pkg"
|
||||
TARGET_DIRECTORY=".build"
|
||||
BINARY_NAME="cloudflared"
|
||||
VERSION=$(git describe --tags --always --dirty="-dev")
|
||||
@@ -25,10 +29,11 @@ INSTALLER_CERT="installer.cer"
|
||||
BUNDLE_ID="com.cloudflare.cloudflared"
|
||||
SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain."
|
||||
export PATH="$PATH:/usr/local/bin"
|
||||
FILENAME="$(pwd)/artifacts/cloudflared-darwin-$TARGET_ARCH.tgz"
|
||||
PKGNAME="$(pwd)/artifacts/cloudflared-$TARGET_ARCH.pkg"
|
||||
mkdir -p ../src/github.com/cloudflare/
|
||||
cp -r . ../src/github.com/cloudflare/cloudflared
|
||||
cd ../src/github.com/cloudflare/cloudflared
|
||||
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
|
||||
|
||||
# Add code signing private key to the key chain
|
||||
if [[ ! -z "$CFD_CODE_SIGN_KEY" ]]; then
|
||||
@@ -138,22 +143,28 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
# cleanup the build directory because the previous execution might have failed without cleaning up.
|
||||
rm -rf "${TARGET_DIRECTORY}"
|
||||
export TARGET_OS="darwin"
|
||||
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
|
||||
|
||||
# sign the cloudflared binary
|
||||
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
|
||||
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
|
||||
|
||||
|
||||
# notarize the binary
|
||||
# TODO: https://jira.cfdata.org/browse/TUN-5789
|
||||
# TODO: TUN-5789
|
||||
fi
|
||||
|
||||
ARCH_TARGET_DIRECTORY="${TARGET_DIRECTORY}/${TARGET_ARCH}-build"
|
||||
# creating build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
mkdir "${TARGET_DIRECTORY}"
|
||||
mkdir "${TARGET_DIRECTORY}/contents"
|
||||
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
|
||||
rm -rf $ARCH_TARGET_DIRECTORY
|
||||
mkdir -p "${ARCH_TARGET_DIRECTORY}"
|
||||
mkdir -p "${ARCH_TARGET_DIRECTORY}/contents"
|
||||
cp -r ".mac_resources/scripts" "${ARCH_TARGET_DIRECTORY}/scripts"
|
||||
|
||||
# copy cloudflared into the build directory
|
||||
cp ${BINARY_NAME} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
|
||||
cp ${BINARY_NAME} "${ARCH_TARGET_DIRECTORY}/contents/${PRODUCT}"
|
||||
|
||||
# compress cloudflared into a tar and gzipped file
|
||||
tar czf "$FILENAME" "${BINARY_NAME}"
|
||||
@@ -162,23 +173,23 @@ tar czf "$FILENAME" "${BINARY_NAME}"
|
||||
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--scripts ${ARCH_TARGET_DIRECTORY}/scripts \
|
||||
--root ${ARCH_TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
--sign "${PKG_SIGN_NAME}" \
|
||||
${PKGNAME}
|
||||
|
||||
# notarize the package
|
||||
# TODO: https://jira.cfdata.org/browse/TUN-5789
|
||||
# TODO: TUN-5789
|
||||
else
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
--scripts ${TARGET_DIRECTORY}/scripts \
|
||||
--root ${TARGET_DIRECTORY}/contents \
|
||||
--scripts ${ARCH_TARGET_DIRECTORY}/scripts \
|
||||
--root ${ARCH_TARGET_DIRECTORY}/contents \
|
||||
--install-location /usr/local/bin \
|
||||
${PKGNAME}
|
||||
fi
|
||||
|
||||
|
||||
# cleaning up the build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
# cleanup build directory because this script is not ran within containers,
|
||||
# which might lead to future issues in subsequent runs.
|
||||
rm -rf "${TARGET_DIRECTORY}"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
rm -rf /tmp/go
|
||||
export GOCACHE=/tmp/gocache
|
||||
rm -rf $GOCACHE
|
||||
|
||||
./.teamcity/install-cloudflare-go.sh
|
||||
|
||||
export PATH="/tmp/go/bin:$PATH"
|
||||
go version
|
||||
which go
|
||||
go env
|
||||
Vendored
+8
-6
@@ -3,15 +3,17 @@ echo $VERSION
|
||||
|
||||
export TARGET_OS=windows
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
export BUILT_ARTIFACT_DIR=built_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
|
||||
cp ./artifacts/cloudflared-windows-$arch.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe
|
||||
cp ./artifacts/cloudflared-windows-$arch.exe ./cloudflared.exe
|
||||
cp $BUILT_ARTIFACT_DIR/cloudflared-windows-$arch.exe ./cloudflared.exe
|
||||
make cloudflared-msi
|
||||
# Copy msi into final directory
|
||||
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
|
||||
done
|
||||
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
|
||||
|
||||
Vendored
-26
@@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
||||
echo "Skipping public release for an untagged commit."
|
||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${HOMEBREW_GITHUB_API_TOKEN:-}" == "" ]] ; then
|
||||
echo "Missing GITHUB_API_TOKEN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# "install" Homebrew
|
||||
git clone https://github.com/Homebrew/brew tmp/homebrew
|
||||
eval "$(tmp/homebrew/bin/brew shellenv)"
|
||||
brew update --force --quiet
|
||||
chmod -R go-w "$(brew --prefix)/share/zsh"
|
||||
|
||||
git config --global user.name "cloudflare-warp-bot"
|
||||
git config --global user.email "warp-bot@cloudflare.com"
|
||||
|
||||
# bump formula pr
|
||||
brew bump-formula-pr cloudflared --version="$VERSION" --no-browse --no-audit
|
||||
Vendored
-66
@@ -1,66 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
|
||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
||||
echo "Skipping public release for an untagged commit."
|
||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ! -f "$FILENAME" ]] ; then
|
||||
echo "Missing $FILENAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${GITHUB_PRIVATE_KEY_B64:-}" == "" ]] ; then
|
||||
echo "Missing GITHUB_PRIVATE_KEY_B64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# upload to s3 bucket for use by Homebrew formula
|
||||
s3cmd \
|
||||
--acl-public --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
s3cmd \
|
||||
--acl-public --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
|
||||
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
|
||||
|
||||
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
|
||||
mkdir -p tmp
|
||||
ssh-keyscan -t rsa github.com > tmp/github.txt
|
||||
echo "$GITHUB_PRIVATE_KEY_B64" | base64 --decode > tmp/private.key
|
||||
chmod 0400 tmp/private.key
|
||||
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
|
||||
|
||||
# clone Homebrew repo into tmp/homebrew-cloudflare
|
||||
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
|
||||
cd tmp/homebrew-cloudflare
|
||||
git checkout -f master
|
||||
git reset --hard origin/master
|
||||
|
||||
# modify cloudflared.rb
|
||||
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Cloudflare Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps'
|
||||
url '$URL'
|
||||
sha256 '$SHA256'
|
||||
version '$VERSION'
|
||||
def install
|
||||
bin.install 'cloudflared'
|
||||
end
|
||||
end
|
||||
EOF
|
||||
|
||||
# push cloudflared.rb
|
||||
git add cloudflared.rb
|
||||
git diff
|
||||
git config user.name "cloudflare-warp-bot"
|
||||
git config user.email "warp-bot@cloudflare.com"
|
||||
git commit -m "Release Cloudflare Tunnel $VERSION"
|
||||
|
||||
git push -v origin master
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
# Relative path to working directory
|
||||
$CloudflaredDirectory = "go\src\github.com\cloudflare\cloudflared"
|
||||
|
||||
cd $CloudflaredDirectory
|
||||
|
||||
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
|
||||
|
||||
& make cloudflared
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for amd64" }
|
||||
copy .\cloudflared.exe .\cloudflared-windows-amd64.exe
|
||||
|
||||
Write-Output "Building for 386"
|
||||
$env:CGO_ENABLED = 0
|
||||
$env:TARGET_ARCH = "386"
|
||||
make cloudflared
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for 386" }
|
||||
copy .\cloudflared.exe .\cloudflared-windows-386.exe
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
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 -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
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
Write-Output "Downloading cloudflare go..."
|
||||
|
||||
Set-Location "$Env:Temp"
|
||||
|
||||
git clone -q https://github.com/cloudflare/go
|
||||
Write-Output "Building go..."
|
||||
cd go/src
|
||||
# https://github.com/cloudflare/go/tree/f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38 is version go1.22.5-devel-cf
|
||||
git checkout -q f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38
|
||||
& ./make.bat
|
||||
|
||||
Write-Output "Installed"
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
$GoMsiVersion = "go1.22.5.windows-amd64.msi"
|
||||
|
||||
Write-Output "Downloading go installer..."
|
||||
|
||||
Set-Location "$Env:Temp"
|
||||
|
||||
(New-Object System.Net.WebClient).DownloadFile(
|
||||
"https://go.dev/dl/$GoMsiVersion",
|
||||
"$Env:Temp\$GoMsiVersion"
|
||||
)
|
||||
|
||||
Write-Output "Installing go..."
|
||||
Install-Package "$Env:Temp\$GoMsiVersion" -Force
|
||||
|
||||
# Go installer updates global $PATH
|
||||
go env
|
||||
|
||||
Write-Output "Installed"
|
||||
+15
@@ -1,3 +1,18 @@
|
||||
## 2024.10.0
|
||||
### Bug Fixes
|
||||
- We fixed a bug related to `--grace-period`. Tunnels that use QUIC as transport weren't abiding by this waiting period before forcefully closing the connections to the edge. From now on, both QUIC and HTTP2 tunnels will wait for either the grace period to end (defaults to 30 seconds) or until the last in-flight request is handled. Users that wish to maintain the previous behavior should set `--grace-period` to 0 if `--protocol` is set to `quic`. This will force `cloudflared` to shutdown as soon as either SIGTERM or SIGINT is received.
|
||||
|
||||
## 2024.2.1
|
||||
### Notices
|
||||
- Starting from this version, tunnel diagnostics will be enabled by default. This will allow the engineering team to remotely get diagnostics from cloudflared during debug activities. Users still have the capability to opt-out of this feature by defining `--management-diagnostics=false` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`).
|
||||
|
||||
## 2023.9.0
|
||||
### Notices
|
||||
- The `warp-routing` `enabled: boolean` flag is no longer supported in the configuration file. Warp Routing traffic (eg TCP, UDP, ICMP) traffic is proxied to cloudflared if routes to the target tunnel are configured. This change does not affect remotely managed tunnels, but for locally managed tunnels, users that might be relying on this feature flag to block traffic should instead guarantee that tunnel has no Private Routes configured for the tunnel.
|
||||
## 2023.7.0
|
||||
### New Features
|
||||
- You can now enable additional diagnostics over the management.argotunnel.com service for your active cloudflared connectors via a new runtime flag `--management-diagnostics` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`). This feature is provided as opt-in and requires the flag to enable. Endpoints such as /metrics provides your prometheus metrics endpoint another mechanism to be reached. Additionally /debug/pprof/(goroutine|heap) are also introduced to allow for remotely retrieving active pprof information from a running cloudflared connector.
|
||||
|
||||
## 2023.4.1
|
||||
### New Features
|
||||
- You can now stream your logs from your remote cloudflared to your local terminal with `cloudflared tail <TUNNEL-ID>`. This new feature requires the remote cloudflared to be version 2023.4.1 or higher.
|
||||
|
||||
+8
-4
@@ -1,19 +1,23 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.19 as builder
|
||||
FROM golang:1.22.5 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
TARGET_GOARCH=${TARGET_GOARCH}
|
||||
|
||||
TARGET_GOARCH=${TARGET_GOARCH} \
|
||||
CONTAINER_BUILD=1
|
||||
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
COPY . .
|
||||
|
||||
RUN .teamcity/install-cloudflare-go.sh
|
||||
|
||||
# compile cloudflared
|
||||
RUN make cloudflared
|
||||
RUN PATH="/tmp/go/bin:$PATH" make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot
|
||||
|
||||
+6
-4
@@ -1,15 +1,17 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.19 as builder
|
||||
FROM golang:1.22.5 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
|
||||
CGO_ENABLED=0
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
COPY . .
|
||||
|
||||
RUN .teamcity/install-cloudflare-go.sh
|
||||
|
||||
# compile cloudflared
|
||||
RUN GOOS=linux GOARCH=amd64 make cloudflared
|
||||
RUN GOOS=linux GOARCH=amd64 PATH="/tmp/go/bin:$PATH" make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot
|
||||
|
||||
+6
-4
@@ -1,15 +1,17 @@
|
||||
# use a builder image for building cloudflare
|
||||
FROM golang:1.19 as builder
|
||||
FROM golang:1.22.5 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
|
||||
CGO_ENABLED=0
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
COPY . .
|
||||
|
||||
RUN .teamcity/install-cloudflare-go.sh
|
||||
|
||||
# compile cloudflared
|
||||
RUN GOOS=linux GOARCH=arm64 make cloudflared
|
||||
RUN GOOS=linux GOARCH=arm64 PATH="/tmp/go/bin:$PATH" make cloudflared
|
||||
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot-arm64
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# The targets cannot be run in parallel
|
||||
.NOTPARALLEL:
|
||||
|
||||
VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
|
||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||
@@ -27,6 +30,10 @@ ifdef PACKAGE_MANAGER
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)"
|
||||
endif
|
||||
|
||||
ifdef CONTAINER_BUILD
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/metrics.Runtime=virtual"
|
||||
endif
|
||||
|
||||
LINK_FLAGS :=
|
||||
ifeq ($(FIPS), true)
|
||||
LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS)
|
||||
@@ -49,6 +56,8 @@ PACKAGE_DIR := $(CURDIR)/packaging
|
||||
PREFIX := /usr
|
||||
INSTALL_BINDIR := $(PREFIX)/bin/
|
||||
INSTALL_MANDIR := $(PREFIX)/share/man/man1/
|
||||
CF_GO_PATH := /tmp/go
|
||||
PATH := $(CF_GO_PATH)/bin:$(PATH)
|
||||
|
||||
LOCAL_ARCH ?= $(shell uname -m)
|
||||
ifneq ($(GOARCH),)
|
||||
@@ -126,7 +135,7 @@ ifeq ($(FIPS), true)
|
||||
$(info Building cloudflared with go-fips)
|
||||
cp -f fips/fips.go.linux-amd64 cmd/cloudflared/fips.go
|
||||
endif
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -v -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
ifeq ($(FIPS), true)
|
||||
rm -f cmd/cloudflared/fips.go
|
||||
./check-fips.sh cloudflared
|
||||
@@ -140,6 +149,7 @@ container:
|
||||
generate-docker-version:
|
||||
echo latest $(VERSION) > versions
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
ifndef CI
|
||||
@@ -147,17 +157,43 @@ ifndef CI
|
||||
else
|
||||
@mkdir -p .cover
|
||||
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
|
||||
go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||
endif
|
||||
|
||||
.PHONY: test-ssh-server
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
.PHONY: cover
|
||||
cover:
|
||||
@echo ""
|
||||
@echo "=====> Total test coverage: <====="
|
||||
@echo ""
|
||||
# Print the overall coverage here for quick access.
|
||||
$Q go tool cover -func ".cover/c.out" | grep "total:" | awk '{print $$3}'
|
||||
# Generate the HTML report that can be viewed from the browser in CI.
|
||||
$Q go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||
|
||||
.PHONY: fuzz
|
||||
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=FuzzRegistrationDatagram -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzPayloadDatagram -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzRegistrationResponseDatagram -fuzztime=600s ./quic/v3
|
||||
@go test -fuzz=FuzzNewIdentity -fuzztime=600s ./tracing
|
||||
@go test -fuzz=FuzzNewAccessValidator -fuzztime=600s ./validation
|
||||
|
||||
.PHONY: install-go
|
||||
install-go:
|
||||
rm -rf ${CF_GO_PATH}
|
||||
./.teamcity/install-cloudflare-go.sh
|
||||
|
||||
.PHONY: cleanup-go
|
||||
cleanup-go:
|
||||
rm -rf ${CF_GO_PATH}
|
||||
|
||||
cloudflared.1: cloudflared_man_template
|
||||
sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' cloudflared_man_template > cloudflared.1
|
||||
|
||||
install: cloudflared cloudflared.1
|
||||
install: install-go cloudflared cloudflared.1 cleanup-go
|
||||
mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR)
|
||||
install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared
|
||||
install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1
|
||||
@@ -194,116 +230,29 @@ cloudflared-pkg: cloudflared cloudflared.1
|
||||
cloudflared-msi:
|
||||
wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
cloudflared-darwin-amd64.tgz: cloudflared
|
||||
tar czf cloudflared-darwin-amd64.tgz cloudflared
|
||||
rm cloudflared
|
||||
|
||||
.PHONY: cloudflared-junos
|
||||
cloudflared-junos: cloudflared jetez-certificate.pem jetez-key.pem
|
||||
jetez --source . \
|
||||
-j jet.yaml \
|
||||
--key jetez-key.pem \
|
||||
--cert jetez-certificate.pem \
|
||||
--version $(VERSION)
|
||||
rm jetez-*.pem
|
||||
|
||||
jetez-certificate.pem:
|
||||
ifndef JETEZ_CERT
|
||||
$(error JETEZ_CERT not defined)
|
||||
endif
|
||||
@echo "Writing JetEZ certificate"
|
||||
@echo "$$JETEZ_CERT" > jetez-certificate.pem
|
||||
|
||||
jetez-key.pem:
|
||||
ifndef JETEZ_KEY
|
||||
$(error JETEZ_KEY not defined)
|
||||
endif
|
||||
@echo "Writing JetEZ key"
|
||||
@echo "$$JETEZ_KEY" > jetez-key.pem
|
||||
|
||||
.PHONY: publish-cloudflared-junos
|
||||
publish-cloudflared-junos: cloudflared-junos cloudflared-x86-64.latest.s3
|
||||
ifndef S3_ENDPOINT
|
||||
$(error S3_HOST not defined)
|
||||
endif
|
||||
ifndef S3_URI
|
||||
$(error S3_URI not defined)
|
||||
endif
|
||||
ifndef S3_ACCESS_KEY
|
||||
$(error S3_ACCESS_KEY not defined)
|
||||
endif
|
||||
ifndef S3_SECRET_KEY
|
||||
$(error S3_SECRET_KEY not defined)
|
||||
endif
|
||||
sha256sum cloudflared-x86-64-$(VERSION).tgz | awk '{printf $$1}' > cloudflared-x86-64-$(VERSION).tgz.shasum
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64-$(VERSION).tgz $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64-$(VERSION).tgz.shasum $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz.shasum
|
||||
dpkg --compare-versions "$(VERSION)" gt "$(shell cat cloudflared-x86-64.latest.s3)" && \
|
||||
echo -n "$(VERSION)" > cloudflared-x86-64.latest && \
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
||||
put cloudflared-x86-64.latest $(S3_URI)/cloudflared-x86-64.latest || \
|
||||
echo "Latest version not updated"
|
||||
|
||||
cloudflared-x86-64.latest.s3:
|
||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force \
|
||||
get $(S3_URI)/cloudflared-x86-64.latest cloudflared-x86-64.latest.s3
|
||||
|
||||
.PHONY: homebrew-upload
|
||||
homebrew-upload: cloudflared-darwin-amd64.tgz
|
||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $$^ $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz
|
||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz $(S3_URI)/cloudflared-stable-$1.tgz
|
||||
|
||||
.PHONY: homebrew-release
|
||||
homebrew-release: homebrew-upload
|
||||
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
|
||||
.PHONY: github-release-dryrun
|
||||
github-release-dryrun:
|
||||
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION) --dry-run
|
||||
|
||||
.PHONY: github-release
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-release-built-pkgs
|
||||
github-release-built-pkgs:
|
||||
github-release:
|
||||
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
|
||||
|
||||
.PHONY: release-pkgs-linux
|
||||
release-pkgs-linux:
|
||||
python3 ./release_pkgs.py
|
||||
|
||||
.PHONY: github-message
|
||||
github-message:
|
||||
python3 github_message.py --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-mac-upload
|
||||
github-mac-upload:
|
||||
python3 github_release.py --path artifacts/cloudflared-darwin-amd64.tgz --release-version $(VERSION) --name cloudflared-darwin-amd64.tgz
|
||||
python3 github_release.py --path artifacts/cloudflared-amd64.pkg --release-version $(VERSION) --name cloudflared-amd64.pkg
|
||||
.PHONY: r2-linux-release
|
||||
r2-linux-release:
|
||||
python3 ./release_pkgs.py
|
||||
|
||||
.PHONY: github-windows-upload
|
||||
github-windows-upload:
|
||||
python3 github_release.py --path built_artifacts/cloudflared-windows-amd64.exe --release-version $(VERSION) --name cloudflared-windows-amd64.exe
|
||||
python3 github_release.py --path built_artifacts/cloudflared-windows-amd64.msi --release-version $(VERSION) --name cloudflared-windows-amd64.msi
|
||||
python3 github_release.py --path built_artifacts/cloudflared-windows-386.exe --release-version $(VERSION) --name cloudflared-windows-386.exe
|
||||
python3 github_release.py --path built_artifacts/cloudflared-windows-386.msi --release-version $(VERSION) --name cloudflared-windows-386.msi
|
||||
|
||||
.PHONY: tunnelrpc-deps
|
||||
tunnelrpc-deps:
|
||||
.PHONY: capnp
|
||||
capnp:
|
||||
which capnp # https://capnproto.org/install.html
|
||||
which capnpc-go # go install zombiezen.com/go/capnproto2/capnpc-go@latest
|
||||
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: quic-deps
|
||||
quic-deps:
|
||||
which capnp
|
||||
which capnpc-go
|
||||
capnp compile -ogo quic/schema/quic_metadata_protocol.capnp
|
||||
capnp compile -ogo tunnelrpc/proto/tunnelrpc.capnp tunnelrpc/proto/quic_metadata_protocol.capnp
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet -v -mod=vendor github.com/cloudflare/cloudflared/...
|
||||
go vet -mod=vendor github.com/cloudflare/cloudflared/...
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
goimports -l -w -local github.com/cloudflare/cloudflared $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc)
|
||||
goimports -l -w -local github.com/cloudflare/cloudflared $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc/proto)
|
||||
|
||||
@@ -31,7 +31,7 @@ Downloads are available as standalone binaries, a Docker image, and Debian, RPM,
|
||||
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux)
|
||||
* A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared)
|
||||
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
|
||||
* Build from source with the [instructions here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#build-from-source)
|
||||
* To build from source, first you need to download the go toolchain by running `./.teamcity/install-cloudflare-go.sh` and follow the output. Then you can run `make cloudflared`
|
||||
|
||||
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
|
||||
@@ -49,13 +49,10 @@ Once installed, you can authenticate `cloudflared` into your Cloudflare account
|
||||
|
||||
## TryCloudflare
|
||||
|
||||
Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare).
|
||||
Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/).
|
||||
|
||||
## Deprecated versions
|
||||
|
||||
Cloudflare currently supports versions of `cloudflared` 2020.5.1 and later. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
||||
Cloudflare currently supports versions of cloudflared that are **within one year** of the most recent release. Breaking changes unrelated to feature availability may be introduced that will impact versions released more than one year ago. You can read more about upgrading cloudflared in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/#updating-cloudflared).
|
||||
|
||||
| Version(s) | Deprecation status |
|
||||
|---|---|
|
||||
| 2020.5.1 and later | Supported |
|
||||
| Versions prior to 2020.5.1 | No longer supported |
|
||||
For example, as of January 2023 Cloudflare will support cloudflared version 2023.1.1 to cloudflared 2022.1.1.
|
||||
|
||||
+299
@@ -1,3 +1,302 @@
|
||||
2024.12.1
|
||||
- 2024-12-10 TUN-8795: update createrepo to createrepo_c to fix the release_pkgs.py script
|
||||
|
||||
2024.12.0
|
||||
- 2024-12-09 TUN-8640: Add ICMP support for datagram V3
|
||||
- 2024-12-09 TUN-8789: make python package installation consistent
|
||||
- 2024-12-06 TUN-8781: Add Trixie, drop Buster. Default to Bookworm
|
||||
- 2024-12-05 TUN-8775: Make sure the session Close can only be called once
|
||||
- 2024-12-04 TUN-8725: implement diagnostic procedure
|
||||
- 2024-12-04 TUN-8767: include raw output from network collector in diagnostic zipfile
|
||||
- 2024-12-04 TUN-8770: add cli configuration and tunnel configuration to diagnostic zipfile
|
||||
- 2024-12-04 TUN-8768: add job report to diagnostic zipfile
|
||||
- 2024-12-03 TUN-8726: implement compression routine to be used in diagnostic procedure
|
||||
- 2024-12-03 TUN-8732: implement port selection algorithm
|
||||
- 2024-12-03 TUN-8762: fix argument order when invoking tracert and modify network info output parsing.
|
||||
- 2024-12-03 TUN-8769: fix k8s log collector arguments
|
||||
- 2024-12-03 TUN-8727: extend client to include function to get cli configuration and tunnel configuration
|
||||
- 2024-11-29 TUN-8729: implement network collection for diagnostic procedure
|
||||
- 2024-11-29 TUN-8727: implement metrics, runtime, system, and tunnelstate in diagnostic http client
|
||||
- 2024-11-27 TUN-8733: add log collection for docker
|
||||
- 2024-11-27 TUN-8734: add log collection for kubernetes
|
||||
- 2024-11-27 TUN-8640: Refactor ICMPRouter to support new ICMPResponders
|
||||
- 2024-11-26 TUN-8735: add managed/local log collection
|
||||
- 2024-11-25 TUN-8728: implement diag/tunnel endpoint
|
||||
- 2024-11-25 TUN-8730: implement diag/configuration
|
||||
- 2024-11-22 TUN-8737: update metrics server port selection
|
||||
- 2024-11-22 TUN-8731: Implement diag/system endpoint
|
||||
- 2024-11-21 TUN-8748: Migrated datagram V3 flows to use migrated context
|
||||
|
||||
2024.11.1
|
||||
- 2024-11-18 Add cloudflared tunnel ready command
|
||||
- 2024-11-14 Make metrics a requirement for tunnel ready command
|
||||
- 2024-11-12 TUN-8701: Simplify flow registration logs for datagram v3
|
||||
- 2024-11-11 add: new go-fuzz targets
|
||||
- 2024-11-07 TUN-8701: Add metrics and adjust logs for datagram v3
|
||||
- 2024-11-06 TUN-8709: Add session migration for datagram v3
|
||||
- 2024-11-04 Fixed 404 in README.md to TryCloudflare
|
||||
- 2024-09-24 Update semgrep.yml
|
||||
|
||||
2024.11.0
|
||||
- 2024-11-05 VULN-66059: remove ssh server tests
|
||||
- 2024-11-04 TUN-8700: Add datagram v3 muxer
|
||||
- 2024-11-04 TUN-8646: Allow experimental feature support for datagram v3
|
||||
- 2024-11-04 TUN-8641: Expose methods to simplify V3 Datagram parsing on the edge
|
||||
- 2024-10-31 TUN-8708: Bump python min version to 3.10
|
||||
- 2024-10-31 TUN-8667: Add datagram v3 session manager
|
||||
- 2024-10-25 TUN-8692: remove dashes from session id
|
||||
- 2024-10-24 TUN-8694: Rework release script
|
||||
- 2024-10-24 TUN-8661: Refactor connection methods to support future different datagram muxing methods
|
||||
- 2024-07-22 TUN-8553: Bump go to 1.22.5 and go-boring 1.22.5-1
|
||||
|
||||
2024.10.1
|
||||
- 2024-10-23 TUN-8694: Fix github release script
|
||||
- 2024-10-21 Revert "TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport"
|
||||
- 2024-10-18 TUN-8688: Correct UDP bind for IPv6 edge connectivity on macOS
|
||||
- 2024-10-17 TUN-8685: Bump coredns dependency
|
||||
- 2024-10-16 TUN-8638: Add datagram v3 serializers and deserializers
|
||||
- 2024-10-15 chore: Remove h2mux code
|
||||
- 2024-10-11 TUN-8631: Abort release on version mismatch
|
||||
|
||||
2024.10.0
|
||||
- 2024-10-01 TUN-8646: Add datagram v3 support feature flag
|
||||
- 2024-09-30 TUN-8621: Fix cloudflared version in change notes to account for release date
|
||||
- 2024-09-19 Adding semgrep yaml file
|
||||
- 2024-09-12 TUN-8632: Delay checking auto-update by the provided frequency
|
||||
- 2024-09-11 TUN-8630: Check checksum of downloaded binary to compare to current for auto-updating
|
||||
- 2024-09-09 TUN-8629: Cloudflared update on Windows requires running it twice to update
|
||||
- 2024-09-06 PPIP-2310: Update quick tunnel disclaimer
|
||||
- 2024-08-30 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering
|
||||
- 2024-08-09 TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport
|
||||
- 2024-06-26 TUN-8484: Print response when QuickTunnel can't be unmarshalled
|
||||
|
||||
2024.9.1
|
||||
- 2024-09-10 Revert Release 2024.9.0
|
||||
|
||||
2024.9.0
|
||||
- 2024-09-10 TUN-8621: Fix cloudflared version in change notes.
|
||||
- 2024-09-06 PPIP-2310: Update quick tunnel disclaimer
|
||||
- 2024-08-30 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering
|
||||
- 2024-08-09 TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport
|
||||
- 2024-06-26 TUN-8484: Print response when QuickTunnel can't be unmarshalled
|
||||
|
||||
2024.8.3
|
||||
- 2024-08-15 TUN-8591 login command without extra text
|
||||
- 2024-03-25 remove code that will not be executed
|
||||
- 2024-03-25 remove code that will not be executed
|
||||
|
||||
2024.8.2
|
||||
- 2024-08-05 TUN-8583: change final directory of artifacts
|
||||
- 2024-08-05 TUN-8585: Avoid creating GH client when dry-run is true
|
||||
|
||||
2024.7.3
|
||||
- 2024-07-31 TUN-8546: Fix final artifacts paths
|
||||
|
||||
2024.7.2
|
||||
- 2024-07-17 TUN-8546: rework MacOS build script
|
||||
|
||||
2024.7.1
|
||||
- 2024-07-16 TUN-8543: use -p flag to create intermediate directories
|
||||
|
||||
2024.7.0
|
||||
- 2024-07-05 TUN-8520: add macos arm64 build
|
||||
- 2024-07-05 TUN-8523: refactor makefile and cfsetup
|
||||
- 2024-07-02 TUN-8504: Use pre-installed python version instead of downloading it on Windows builds
|
||||
- 2024-06-26 TUN-8489: Add default noop logger for capnprpc
|
||||
- 2024-06-25 TUN-8487: Add user-agent for quick-tunnel requests
|
||||
- 2023-12-12 TUN-8057: cloudflared uses new PQ curve ID
|
||||
|
||||
2024.6.1
|
||||
- 2024-06-12 TUN-8461: Don't log Failed to send session payload if the error is EOF
|
||||
- 2024-06-07 TUN-8456: Update quic-go to 0.45 and collect mtu and congestion control metrics
|
||||
- 2024-06-06 TUN-8452: Add flag to control QUIC stream-level flow control limit
|
||||
- 2024-06-06 TUN-8451: Log QUIC flow control frames and transport parameters received
|
||||
- 2024-06-05 TUN-8449: Add flag to control QUIC connection-level flow control limit and increase default to 30MB
|
||||
|
||||
2024.6.0
|
||||
- 2024-05-30 TUN-8441: Correct UDP total sessions metric to a counter and add new ICMP metrics
|
||||
- 2024-05-28 TUN-8422: Add metrics for capnp method calls
|
||||
- 2024-05-24 TUN-8424: Refactor capnp registration server
|
||||
- 2024-05-23 TUN-8427: Fix BackoffHandler's internally shared clock structure
|
||||
- 2024-05-21 TUN-8425: Remove ICMP binding for quick tunnels
|
||||
- 2024-05-20 TUN-8423: Deprecate older legacy tunnel capnp interfaces
|
||||
- 2024-05-15 TUN-8419: Add capnp safe transport
|
||||
- 2024-05-13 TUN-8415: Refactor capnp rpc into a single module
|
||||
|
||||
2024.5.0
|
||||
- 2024-05-07 TUN-8407: Upgrade go to version 1.22.2
|
||||
|
||||
2024.4.1
|
||||
- 2024-04-22 TUN-8380: Add sleep before requesting quick tunnel as temporary fix for component tests
|
||||
- 2024-04-19 TUN-8374: Close UDP socket if registration fails
|
||||
- 2024-04-18 TUN-8371: Bump quic-go to v0.42.0
|
||||
- 2024-04-03 TUN-8333: Bump go-jose dependency to v4
|
||||
- 2024-04-02 TUN-8331: Add unit testing for AccessJWTValidator middleware
|
||||
|
||||
2024.4.0
|
||||
- 2024-04-02 feat: provide short version (#1206)
|
||||
- 2024-04-02 Format code
|
||||
- 2024-01-18 feat: auto tls sni
|
||||
- 2023-12-24 fix checkInPingGroup bugs
|
||||
- 2023-12-15 Add environment variables for TCP tunnel hostname / destination / URL.
|
||||
|
||||
2024.3.0
|
||||
- 2024-03-14 TUN-8281: Run cloudflared query list tunnels/routes endpoint in a paginated way
|
||||
- 2024-03-13 TUN-8297: Improve write timeout logging on safe_stream.go
|
||||
- 2024-03-07 TUN-8290: Remove `|| true` from postrm.sh
|
||||
- 2024-03-05 TUN-8275: Skip write timeout log on "no network activity"
|
||||
- 2024-01-23 Update postrm.sh to fix incomplete uninstall
|
||||
- 2024-01-05 fix typo in errcheck for response parsing logic in CreateTunnel routine
|
||||
- 2023-12-23 Update linux_service.go
|
||||
- 2023-12-07 ci: bump actions/checkout to v4
|
||||
- 2023-12-07 ci/check: bump actions/setup-go to v5
|
||||
- 2023-04-28 check.yaml: bump actions/setup-go to v4
|
||||
|
||||
2024.2.1
|
||||
- 2024-02-20 TUN-8242: Update Changes.md file with new remote diagnostics behaviour
|
||||
- 2024-02-19 TUN-8238: Fix type mismatch introduced by fast-forward
|
||||
- 2024-02-16 TUN-8243: Collect metrics on the number of QUIC frames sent/received
|
||||
- 2024-02-15 TUN-8238: Refactor proxy logging
|
||||
- 2024-02-14 TUN-8242: Enable remote diagnostics by default
|
||||
- 2024-02-12 TUN-8236: Add write timeout to quic and tcp connections
|
||||
- 2024-02-09 TUN-8224: Fix safety of TCP stream logging, separate connect and ack log messages
|
||||
|
||||
2024.2.0
|
||||
- 2024-02-07 TUN-8224: Count and collect metrics on stream connect successes/errors
|
||||
|
||||
2024.1.5
|
||||
- 2024-01-22 TUN-8176: Support ARM platforms that don't have an FPU or have it enabled in kernel
|
||||
- 2024-01-15 TUN-8158: Bring back commit e6537418859afcac29e56a39daa08bcabc09e048 and fixes infinite loop on linux when the socket is closed
|
||||
|
||||
2024.1.4
|
||||
- 2024-01-19 Revert "TUN-8158: Add logging to confirm when ICMP reply is returned to the edge"
|
||||
|
||||
2024.1.3
|
||||
- 2024-01-15 TUN-8161: Fix broken ARM build for armv6
|
||||
- 2024-01-15 TUN-8158: Add logging to confirm when ICMP reply is returned to the edge
|
||||
|
||||
2024.1.2
|
||||
- 2024-01-11 TUN-8147: Disable ECN usage due to bugs in detecting if supported
|
||||
- 2024-01-11 TUN-8146: Fix export path for install-go command
|
||||
- 2024-01-11 TUN-8146: Fix Makefile targets should not be run in parallel and install-go script was missing shebang
|
||||
- 2024-01-10 TUN-8140: Remove homebrew scripts
|
||||
|
||||
2024.1.1
|
||||
- 2024-01-10 TUN-8134: Revert installed prefix to /usr
|
||||
- 2024-01-09 TUN-8130: Fix path to install go for mac build
|
||||
- 2024-01-09 TUN-8129: Use the same build command between branch and release builds
|
||||
- 2024-01-09 TUN-8130: Install go tool chain in /tmp on build agents
|
||||
- 2024-01-09 TUN-8134: Install cloudflare go as part of make install
|
||||
- 2024-01-08 TUN-8118: Disable FIPS module to build with go-boring without CGO_ENABLED
|
||||
|
||||
2024.1.0
|
||||
- 2024-01-01 TUN-7934: Update quic-go to a version that queues datagrams for better throughput and drops large datagram
|
||||
- 2023-12-20 TUN-8072: Need to set GOCACHE in mac go installation script
|
||||
- 2023-12-17 TUN-8072: Add script to download cloudflare go for Mac build agents
|
||||
- 2023-12-15 Fix nil pointer dereference segfault when passing "null" config json to cloudflared tunnel ingress validate (#1070)
|
||||
- 2023-12-15 configuration.go: fix developerPortal link (#960)
|
||||
- 2023-12-14 tunnelrpc/pogs: fix dropped test errors (#1106)
|
||||
- 2023-12-14 cmd/cloudflared/updater: fix dropped error (#1055)
|
||||
- 2023-12-14 use os.Executable to discover the path to cloudflared (#1040)
|
||||
- 2023-12-14 Remove extraneous `period` from Path Environment Variable (#1009)
|
||||
- 2023-12-14 Use CLI context when running tunnel (#597)
|
||||
- 2023-12-14 TUN-8066: Define scripts to build on Windows agents
|
||||
- 2023-12-11 TUN-8052: Update go to 1.21.5
|
||||
- 2023-12-07 TUN-7970: Default to enable post quantum encryption for quic transport
|
||||
- 2023-12-04 TUN-8006: Update quic-go to latest upstream
|
||||
- 2023-11-15 VULN-44842 Add a flag that allows users to not send the Access JWT to stdout
|
||||
- 2023-11-13 TUN-7965: Remove legacy incident status page check
|
||||
- 2023-11-13 AUTH-5682 Org token flow in Access logins should pass CF_AppSession cookie
|
||||
|
||||
2023.10.0
|
||||
- 2023-10-06 TUN-7864: Document cloudflared versions support
|
||||
- 2023-10-03 CUSTESC-33731: Make rule match test report rule in 0-index base
|
||||
- 2023-09-22 TUN-7824: Fix usage of systemctl status to detect which services are installed
|
||||
- 2023-09-20 TUN-7813: Improve tunnel delete command to use cascade delete
|
||||
- 2023-09-20 TUN-7787: cloudflared only list ip routes targeted for cfd_tunnel
|
||||
- 2023-09-15 TUN-7787: Refactor cloudflared to use new route endpoints based on route IDs
|
||||
- 2023-09-08 TUN-7776: Remove warp-routing flag from cloudflared
|
||||
- 2023-09-05 TUN-7756: Clarify that QUIC is mandatory to support ICMP proxying
|
||||
|
||||
2023.8.2
|
||||
- 2023-08-25 TUN-7700: Implement feature selector to determine if connections will prefer post quantum cryptography
|
||||
- 2023-08-22 TUN-7707: Use X25519Kyber768Draft00 curve when post-quantum feature is enabled
|
||||
|
||||
2023.8.1
|
||||
- 2023-08-23 TUN-7718: Update R2 Token to no longer encode secret
|
||||
|
||||
2023.8.0
|
||||
- 2023-07-26 TUN-7584: Bump go 1.20.6
|
||||
|
||||
2023.7.3
|
||||
- 2023-07-25 TUN-7628: Correct Host parsing for Access
|
||||
- 2023-07-24 TUN-7624: Fix flaky TestBackoffGracePeriod test in cloudflared
|
||||
|
||||
2023.7.2
|
||||
- 2023-07-19 TUN-7599: Onboard cloudflared to Software Dashboard
|
||||
- 2023-07-19 TUN-7587: Remove junos builds
|
||||
- 2023-07-18 TUN-7597: Add flag to disable auto-update services to be installed
|
||||
- 2023-07-17 TUN-7594: Add nightly arm64 cloudflared internal deb publishes
|
||||
- 2023-07-14 TUN-7586: Upgrade go-jose/go-jose/v3 and core-os/go-oidc/v3
|
||||
- 2023-07-14 TUN-7589: Remove legacy golang.org/x/crypto/ssh/terminal package usage
|
||||
- 2023-07-14 TUN-7590: Remove usages of ioutil
|
||||
- 2023-07-14 TUN-7585: Remove h2mux compression
|
||||
- 2023-07-14 TUN-7588: Update package coreos/go-systemd
|
||||
|
||||
2023.7.1
|
||||
- 2023-07-13 TUN-7582: Correct changelog wording for --management-diagnostics
|
||||
- 2023-07-12 TUN-7575: Add option to disable PTMU discovery over QUIC
|
||||
|
||||
2023.7.0
|
||||
- 2023-07-06 TUN-7558: Flush on Writes for StreamBasedOriginProxy
|
||||
- 2023-07-05 TUN-7553: Add flag to enable management diagnostic services
|
||||
- 2023-07-05 TUN-7564: Support cf-trace-id for cloudflared access
|
||||
- 2023-07-05 TUN-7477: Decrement UDP sessions on shutdown
|
||||
- 2023-07-03 TUN-7545: Add support for full bidirectionally streaming with close signal propagation
|
||||
- 2023-06-30 TUN-7549: Add metrics route to management service
|
||||
- 2023-06-30 TUN-7551: Complete removal of raven-go to sentry-go
|
||||
- 2023-06-30 TUN-7550: Add pprof endpoint to management service
|
||||
- 2023-06-29 TUN-7543: Add --debug-stream flag to cloudflared access ssh
|
||||
- 2023-06-26 TUN-6011: Remove docker networks from ICMP Proxy test
|
||||
- 2023-06-20 AUTH-5328 Pass cloudflared_token_check param when running cloudflared access login
|
||||
|
||||
2023.6.1
|
||||
- 2023-06-19 TUN-7480: Added a timeout for unregisterUDP.
|
||||
- 2023-06-16 TUN-7477: Add UDP/TCP session metrics
|
||||
- 2023-06-14 TUN-7468: Increase the limit of incoming streams
|
||||
|
||||
2023.6.0
|
||||
- 2023-06-15 TUN-7471: Fixes cloudflared not closing the quic stream on unregister UDP session
|
||||
- 2023-06-09 TUN-7463: Add default ingress rule if no ingress rules are provided when updating the configuration
|
||||
- 2023-05-31 TUN-7447: Add a cover build to report code coverage
|
||||
|
||||
2023.5.1
|
||||
- 2023-05-16 TUN-7424: Add CORS headers to host_details responses
|
||||
- 2023-05-11 TUN-7421: Add *.cloudflare.com to permitted Origins for management WebSocket requests
|
||||
- 2023-05-05 TUN-7404: Default configuration version set to -1
|
||||
- 2023-05-05 TUN-7227: Migrate to devincarr/quic-go
|
||||
|
||||
2023.5.0
|
||||
- 2023-04-27 TUN-7398: Add support for quic safe stream to set deadline
|
||||
- 2023-04-26 TUN-7394: Retry StartFirstTunnel on quic.ApplicationErrors
|
||||
- 2023-04-26 TUN-7392: Ignore release checksum upload if asset already uploaded
|
||||
- 2023-04-25 TUN-7392: Ignore duplicate artifact uploads for github release
|
||||
- 2023-04-25 TUN-7393: Add json output for cloudflared tail
|
||||
- 2023-04-24 TUN-7390: Remove Debian stretch builds
|
||||
|
||||
2023.4.2
|
||||
- 2023-04-24 TUN-7133: Add sampling support for streaming logs
|
||||
- 2023-04-21 TUN-7141: Add component tests for streaming logs
|
||||
- 2023-04-21 TUN-7373: Streaming logs override for same actor
|
||||
- 2023-04-20 TUN-7383: Bump requirements.txt
|
||||
- 2023-04-19 TUN-7361: Add a label to override hostname
|
||||
- 2023-04-19 TUN-7378: Remove RPC debug logs
|
||||
- 2023-04-18 TUN-7360: Add Get Host Details handler in management service
|
||||
- 2023-04-17 AUTH-3122 Verify that Access tokens are still valid in curl command
|
||||
- 2023-04-17 TUN-7129: Categorize TCP logs for streaming logs
|
||||
- 2023-04-17 TUN-7130: Categorize UDP logs for streaming logs
|
||||
- 2023-04-10 AUTH-4887 Add aud parameter to token transfer url
|
||||
|
||||
2023.4.1
|
||||
- 2023-04-13 TUN-7368: Report destination address for TCP requests in logs
|
||||
- 2023-04-12 TUN-7134: Acquire token for cloudflared tail
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/bin/bash
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
export ARTIFACT_DIR=artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
|
||||
arch=("amd64")
|
||||
@@ -22,4 +23,4 @@ make cloudflared-rpm
|
||||
mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm
|
||||
|
||||
# finally move the linux binary as well.
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch
|
||||
|
||||
+10
-2
@@ -1,11 +1,13 @@
|
||||
#!/bin/bash
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# Avoid depending on C code since we don't need it.
|
||||
# Disable FIPS module in go-boring
|
||||
export GOEXPERIMENT=noboringcrypto
|
||||
export CGO_ENABLED=0
|
||||
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
export ARTIFACT_DIR=artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
|
||||
linuxArchs=("386" "amd64" "arm" "armhf" "arm64")
|
||||
@@ -13,6 +15,12 @@ export TARGET_OS=linux
|
||||
for arch in ${linuxArchs[@]}; do
|
||||
unset TARGET_ARM
|
||||
export TARGET_ARCH=$arch
|
||||
|
||||
## Support for arm platforms without hardware FPU enabled
|
||||
if [[ $arch == arm ]] ; then
|
||||
export TARGET_ARCH=arm
|
||||
export TARGET_ARM=5
|
||||
fi
|
||||
|
||||
## Support for armhf builds
|
||||
if [[ $arch == armhf ]] ; then
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: cloudflared
|
||||
description: Client for Cloudflare Tunnels
|
||||
annotations:
|
||||
backstage.io/source-location: url:https://bitbucket.cfdata.org/projects/TUN/repos/cloudflared/browse
|
||||
cloudflare.com/software-excellence-opt-in: "true"
|
||||
cloudflare.com/jira-project-key: "TUN"
|
||||
cloudflare.com/jira-project-component: "Cloudflare Tunnel"
|
||||
tags:
|
||||
- internal
|
||||
spec:
|
||||
type: "service"
|
||||
lifecycle: "Active"
|
||||
owner: "teams/tunnel-teams-routing"
|
||||
+69
-8
@@ -109,20 +109,34 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (
|
||||
return r.client.Do(req)
|
||||
}
|
||||
|
||||
func parseResponse(reader io.Reader, data interface{}) error {
|
||||
func parseResponseEnvelope(reader io.Reader) (*response, error) {
|
||||
// Schema for Tunnelstore responses in the v1 API.
|
||||
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
||||
var result response
|
||||
// First, parse the wrapper and check the API call succeeded
|
||||
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
||||
return errors.Wrap(err, "failed to decode response")
|
||||
return nil, errors.Wrap(err, "failed to decode response")
|
||||
}
|
||||
if err := result.checkErrors(); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if !result.Success {
|
||||
return ErrAPINoSuccess
|
||||
return nil, ErrAPINoSuccess
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func parseResponse(reader io.Reader, data interface{}) error {
|
||||
result, err := parseResponseEnvelope(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return parseResponseBody(result, data)
|
||||
}
|
||||
|
||||
func parseResponseBody(result *response, data interface{}) error {
|
||||
// At this point we know the API call succeeded, so, parse out the inner
|
||||
// result into the datatype provided as a parameter.
|
||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||
@@ -131,11 +145,58 @@ func parseResponse(reader io.Reader, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T, error) {
|
||||
page := 0
|
||||
var fullResponse []*T
|
||||
|
||||
for {
|
||||
page += 1
|
||||
envelope, parsedBody, err := fetchPage[T](requestFn, page)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("Error Parsing page %d", page))
|
||||
}
|
||||
|
||||
fullResponse = append(fullResponse, parsedBody...)
|
||||
if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
return fullResponse, nil
|
||||
}
|
||||
|
||||
func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*response, []*T, error) {
|
||||
pageResp, err := requestFn(page)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer pageResp.Body.Close()
|
||||
if pageResp.StatusCode == http.StatusOK {
|
||||
envelope, err := parseResponseEnvelope(pageResp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var parsedRspBody []*T
|
||||
return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
|
||||
|
||||
}
|
||||
return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Pagination Pagination `json:"result_info,omitempty"`
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Count int `json:"count,omitempty"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PerPage int `json:"per_page,omitempty"`
|
||||
TotalCount int `json:"total_count,omitempty"`
|
||||
}
|
||||
|
||||
func (r *response) checkErrors() error {
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ type TunnelClient interface {
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
GetTunnelToken(tunnelID uuid.UUID) (string, error)
|
||||
GetManagementToken(tunnelID uuid.UUID) (string, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
DeleteTunnel(tunnelID uuid.UUID, cascade bool) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||
@@ -22,7 +22,7 @@ type HostnameClient interface {
|
||||
type IPRouteClient interface {
|
||||
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
||||
AddRoute(newRoute NewRoute) (Route, error)
|
||||
DeleteRoute(params DeleteRouteParams) error
|
||||
DeleteRoute(id uuid.UUID) error
|
||||
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
||||
}
|
||||
|
||||
|
||||
+23
-28
@@ -75,10 +75,12 @@ type NewRoute struct {
|
||||
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
Network string `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
Comment string `json:"comment"`
|
||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||
}{
|
||||
Network: r.Network.String(),
|
||||
TunnelID: r.TunnelID,
|
||||
Comment: r.Comment,
|
||||
VNetID: r.VNetID,
|
||||
@@ -87,6 +89,7 @@ func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||
|
||||
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
||||
type DetailedRoute struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Network CIDR `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
|
||||
@@ -115,7 +118,8 @@ func (r DetailedRoute) TableString() string {
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
r.ID,
|
||||
r.Network.String(),
|
||||
vnetColumn,
|
||||
r.Comment,
|
||||
@@ -126,12 +130,6 @@ func (r DetailedRoute) TableString() string {
|
||||
)
|
||||
}
|
||||
|
||||
type DeleteRouteParams struct {
|
||||
Network net.IPNet
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
type GetRouteByIpParams struct {
|
||||
Ip net.IP
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
@@ -139,26 +137,30 @@ type GetRouteByIpParams struct {
|
||||
}
|
||||
|
||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||
// Due to pagination on the server side it will call the endpoint multiple times if needed.
|
||||
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
fetchFn := func(page int) (*http.Response, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
filter.Page(page)
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
rsp, err := r.sendRequest("GET", endpoint, nil)
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListDetailedRoutes(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
rsp.Body.Close()
|
||||
return nil, r.statusCodeToError("list routes", rsp)
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list routes", resp)
|
||||
return fetchExhaustively[DetailedRoute](fetchFn)
|
||||
}
|
||||
|
||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
||||
endpoint.Path = path.Join(endpoint.Path)
|
||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||
if err != nil {
|
||||
return Route{}, errors.Wrap(err, "REST request failed")
|
||||
@@ -173,10 +175,9 @@ func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||
}
|
||||
|
||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
|
||||
func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
@@ -211,12 +212,6 @@ func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
||||
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||
}
|
||||
|
||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
||||
var routes []*DetailedRoute
|
||||
err := parseResponse(body, &routes)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func parseRoute(body io.ReadCloser) (Route, error) {
|
||||
var route Route
|
||||
err := parseResponse(body, &route)
|
||||
|
||||
+29
-18
@@ -58,31 +58,29 @@ type IpRouteFilter struct {
|
||||
|
||||
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||
f := &IpRouteFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
f := NewIPRouteFilter()
|
||||
|
||||
// Set deletion filter
|
||||
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
f.deleted()
|
||||
f.Deleted()
|
||||
} else {
|
||||
f.notDeleted()
|
||||
f.NotDeleted()
|
||||
}
|
||||
|
||||
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if subset != nil {
|
||||
f.networkIsSupersetOf(*subset)
|
||||
f.NetworkIsSupersetOf(*subset)
|
||||
}
|
||||
|
||||
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if superset != nil {
|
||||
f.networkIsSupersetOf(*superset)
|
||||
f.NetworkIsSupersetOf(*superset)
|
||||
}
|
||||
|
||||
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
||||
f.commentIs(comment)
|
||||
f.CommentIs(comment)
|
||||
}
|
||||
|
||||
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
||||
@@ -90,7 +88,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
||||
}
|
||||
f.tunnelID(u)
|
||||
f.TunnelID(u)
|
||||
}
|
||||
|
||||
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
||||
@@ -98,7 +96,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
||||
}
|
||||
f.vnetID(u)
|
||||
f.VNetID(u)
|
||||
}
|
||||
|
||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||
@@ -124,35 +122,44 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||
return subset, nil
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) commentIs(comment string) {
|
||||
func NewIPRouteFilter() *IpRouteFilter {
|
||||
values := &IpRouteFilter{queryParams: url.Values{}}
|
||||
|
||||
// always list cfd_tunnel routes only
|
||||
values.queryParams.Set("tun_types", "cfd_tunnel")
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) CommentIs(comment string) {
|
||||
f.queryParams.Set("comment", comment)
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) notDeleted() {
|
||||
func (f *IpRouteFilter) NotDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) deleted() {
|
||||
func (f *IpRouteFilter) Deleted() {
|
||||
f.queryParams.Set("is_deleted", "true")
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
|
||||
func (f *IpRouteFilter) NetworkIsSubsetOf(superset net.IPNet) {
|
||||
f.queryParams.Set("network_subset", superset.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
|
||||
func (f *IpRouteFilter) NetworkIsSupersetOf(subset net.IPNet) {
|
||||
f.queryParams.Set("network_superset", subset.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
|
||||
func (f *IpRouteFilter) ExistedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
|
||||
func (f *IpRouteFilter) TunnelID(id uuid.UUID) {
|
||||
f.queryParams.Set("tunnel_id", id.String())
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
|
||||
func (f *IpRouteFilter) VNetID(id uuid.UUID) {
|
||||
f.queryParams.Set("virtual_network_id", id.String())
|
||||
}
|
||||
|
||||
@@ -160,6 +167,10 @@ func (f *IpRouteFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f *IpRouteFilter) Page(page int) {
|
||||
f.queryParams.Set("page", strconv.Itoa(page))
|
||||
}
|
||||
|
||||
func (f IpRouteFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
`{
|
||||
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"comment":"test",
|
||||
@@ -80,6 +81,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||
},
|
||||
{
|
||||
`{
|
||||
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
|
||||
@@ -167,9 +169,10 @@ func TestRouteTableString(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, network)
|
||||
r := DetailedRoute{
|
||||
ID: uuid.Nil,
|
||||
Network: CIDR(*network),
|
||||
}
|
||||
row := r.TableString()
|
||||
fmt.Println(row)
|
||||
require.True(t, strings.HasPrefix(row, "1.2.3.4/32"))
|
||||
require.True(t, strings.HasPrefix(row, "00000000-0000-0000-0000-000000000000\t1.2.3.4/32"))
|
||||
}
|
||||
|
||||
+21
-19
@@ -93,7 +93,7 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWith
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var tunnel TunnelWithToken
|
||||
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil {
|
||||
if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
|
||||
return nil, serdeErr
|
||||
}
|
||||
return &tunnel, nil
|
||||
@@ -159,9 +159,14 @@ func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err e
|
||||
return "", r.statusCodeToError("get tunnel token", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
// Cascade will delete all tunnel dependencies (connections, routes, etc.) that
|
||||
// are linked to the deleted tunnel.
|
||||
if cascade {
|
||||
endpoint.RawQuery = "cascade=true"
|
||||
}
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
@@ -172,25 +177,22 @@ func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = filter.encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListTunnels(resp.Body)
|
||||
fetchFn := func(page int) (*http.Response, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
filter.Page(page)
|
||||
endpoint.RawQuery = filter.encode()
|
||||
rsp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
rsp.Body.Close()
|
||||
return nil, r.statusCodeToError("list tunnels", rsp)
|
||||
}
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list tunnels", resp)
|
||||
}
|
||||
|
||||
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
||||
var tunnels []*Tunnel
|
||||
err := parseResponse(body, &tunnels)
|
||||
return tunnels, err
|
||||
return fetchExhaustively[Tunnel](fetchFn)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||
|
||||
@@ -50,6 +50,10 @@ func (f *TunnelFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f *TunnelFilter) Page(page int) {
|
||||
f.queryParams.Set("page", strconv.Itoa(page))
|
||||
}
|
||||
|
||||
func (f TunnelFilter) encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package cfapi
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -16,52 +15,6 @@ import (
|
||||
|
||||
var loc, _ = time.LoadLocation("UTC")
|
||||
|
||||
func Test_parseListTunnels(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []*Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": []}`},
|
||||
want: []*Tunnel{},
|
||||
},
|
||||
{
|
||||
name: "success is false",
|
||||
args: args{body: `{"success": false, "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors are present",
|
||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response",
|
||||
args: args{body: `abc`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
||||
got, err := parseListTunnels(body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unmarshalTunnel(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
|
||||
+115
-133
@@ -1,92 +1,69 @@
|
||||
pinned_go: &pinned_go go=1.19.6-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.19.6-1
|
||||
pinned_go: &pinned_go go-boring=1.22.5-1
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: bullseye
|
||||
stretch: &stretch
|
||||
build:
|
||||
default-flavor: bookworm
|
||||
|
||||
bullseye: &bullseye
|
||||
build-linux:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deps
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- libffi-dev
|
||||
pre-cache: &build_pre_cache
|
||||
- export GOCACHE=/cfsetup_build/.cache/go-build
|
||||
- go install golang.org/x/tools/cmd/goimports@latest
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
build-fips:
|
||||
# Build binary for component test
|
||||
- GOOS=linux GOARCH=amd64 make cloudflared
|
||||
build-linux-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deps_fips
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
builddeps: *build_deps
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
|
||||
github-release-pkgs:
|
||||
# Build binary for component test
|
||||
- GOOS=linux GOARCH=amd64 make cloudflared
|
||||
cover:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: *build_deps
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- make cover
|
||||
# except FIPS and macos
|
||||
build-linux-release:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deps_release
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-dev
|
||||
- python3-pip
|
||||
- reprepro
|
||||
- createrepo
|
||||
pre-cache: &github_release_pkgs_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
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
- pip3 install boto3==1.22.9
|
||||
- pip3 install python-gnupg==0.4.9
|
||||
- python3-setuptools
|
||||
- wget
|
||||
- python3-venv
|
||||
post-cache:
|
||||
- python3 -m venv env
|
||||
- . /cfsetup_build/env/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55 boto3==1.22.9 python-gnupg==0.4.9
|
||||
# build all packages (except macos and FIPS) and move them to /cfsetup/built_artifacts
|
||||
- ./build-packages.sh
|
||||
# release the packages built and moved to /cfsetup/built_artifacts
|
||||
- make github-release-built-pkgs
|
||||
# publish packages to linux repos
|
||||
- make release-pkgs-linux
|
||||
# handle FIPS separately so that we built with gofips compiler
|
||||
github-fips-release-pkgs:
|
||||
build-linux-fips-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *github_release_pkgs_pre_cache
|
||||
builddeps: *build_deps_release
|
||||
post-cache:
|
||||
# same logic as above, but for FIPS packages only
|
||||
- ./build-packages-fips.sh
|
||||
- make github-release-built-pkgs
|
||||
generate-versions-file:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
@@ -105,7 +82,7 @@ stretch: &stretch
|
||||
build-fips-internal-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_fips_deb_deps
|
||||
- *pinned_go_fips
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
@@ -115,7 +92,7 @@ stretch: &stretch
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-fips-internal-deb-nightly:
|
||||
build-internal-deb-nightly-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
@@ -125,6 +102,16 @@ stretch: &stretch
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-internal-deb-nightly-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- export NIGHTLY=true
|
||||
# - export FIPS=true # TUN-7595
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
@@ -132,21 +119,7 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
github-release-macos-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_pygithub
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
- make github-mac-upload
|
||||
github-release-windows:
|
||||
package-windows:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
@@ -159,17 +132,25 @@ stretch: &stretch
|
||||
# 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
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
- python3 -m venv env
|
||||
- . env/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55
|
||||
- .teamcity/package-windows.sh
|
||||
- make github-windows-upload
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deps
|
||||
builddeps: &build_deps_tests
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- libffi-dev
|
||||
- gotest-to-teamcity
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
@@ -179,7 +160,7 @@ stretch: &stretch
|
||||
- make test | gotest-to-teamcity
|
||||
test-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deps_fips
|
||||
builddeps: *build_deps_tests
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
@@ -190,84 +171,85 @@ stretch: &stretch
|
||||
- make test | gotest-to-teamcity
|
||||
component-test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: &build_deps_component_test
|
||||
- *pinned_go
|
||||
- python3.7
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-setuptools
|
||||
# procps installs the ps command which is needed in test_sysv_service because the init script
|
||||
# uses ps pid to determine if the agent is running
|
||||
# procps installs the ps command which is needed in test_sysv_service
|
||||
# because the init script uses ps pid to determine if the agent is
|
||||
# running
|
||||
- procps
|
||||
- python3-venv
|
||||
pre-cache-copy-paths:
|
||||
- component-tests/requirements.txt
|
||||
pre-cache: &component_test_pre_cache
|
||||
- sudo pip3 install --upgrade -r component-tests/requirements.txt
|
||||
post-cache: &component_test_post_cache
|
||||
# Creates and routes a Named Tunnel for this build. Also constructs config file from env vars.
|
||||
- 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
|
||||
- pytest component-tests -o log_cli=true --log-cli-level=INFO
|
||||
# The Named Tunnel is deleted and its route unprovisioned here.
|
||||
- python3 component-tests/setup.py --type cleanup
|
||||
component-test-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- python3.7
|
||||
- python3-pip
|
||||
- python3-setuptools
|
||||
# procps installs the ps command which is needed in test_sysv_service because the init script
|
||||
# uses ps pid to determine if the agent is running
|
||||
- procps
|
||||
builddeps: *build_deps_component_test
|
||||
pre-cache-copy-paths:
|
||||
- component-tests/requirements.txt
|
||||
pre-cache: *component_test_pre_cache
|
||||
post-cache: *component_test_post_cache
|
||||
update-homebrew:
|
||||
builddeps:
|
||||
- openssh-client
|
||||
- s3cmd
|
||||
- jq
|
||||
- build-essential
|
||||
- procps
|
||||
post-cache:
|
||||
- .teamcity/update-homebrew.sh
|
||||
- .teamcity/update-homebrew-core.sh
|
||||
github-message-release:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_pygithub
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- make github-message
|
||||
build-junos:
|
||||
github-release-dryrun:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
post-cache:
|
||||
- export CGO_ENABLED=0
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
publish-junos:
|
||||
- python3 -m venv env
|
||||
- . env/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55
|
||||
- make github-release-dryrun
|
||||
github-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
- s4cmd
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
post-cache:
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make publish-cloudflared-junos
|
||||
- python3 -m venv env
|
||||
- . env/bin/activate
|
||||
- pip install pynacl==1.4.0 pygithub==1.55
|
||||
- make github-release
|
||||
r2-linux-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
- reprepro
|
||||
- createrepo-c
|
||||
- python3-venv
|
||||
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-linux-release
|
||||
|
||||
buster: *stretch
|
||||
bullseye: *stretch
|
||||
bookworm: *stretch
|
||||
bookworm: *bullseye
|
||||
trixie: *bullseye
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@
|
||||
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
||||
<Environment Id="ENV0"
|
||||
Name="PATH"
|
||||
Value="[INSTALLDIR]."
|
||||
Value="[INSTALLDIR]"
|
||||
Permanent="no"
|
||||
Part="last"
|
||||
Action="create"
|
||||
|
||||
@@ -3,6 +3,7 @@ package access
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/stream"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
@@ -38,6 +40,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||
if forwarder.TokenSecret != "" {
|
||||
headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret)
|
||||
}
|
||||
headers.Set("User-Agent", userAgent)
|
||||
|
||||
carrier.SetBastionDest(headers, forwarder.Destination)
|
||||
|
||||
@@ -58,31 +61,37 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||
// useful for proxying other protocols (like ssh) over websockets
|
||||
// (which you can put Access in front of)
|
||||
func ssh(c *cli.Context) error {
|
||||
log := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
// If not running as a forwarder, disable terminal logs as it collides with the stdin/stdout of the parent process
|
||||
outputTerminal := logger.DisableTerminalLog
|
||||
if c.IsSet(sshURLFlag) {
|
||||
outputTerminal = logger.EnableTerminalLog
|
||||
}
|
||||
log := logger.CreateSSHLoggerFromContext(c, outputTerminal)
|
||||
|
||||
// get the hostname from the cmdline and error out if its not provided
|
||||
rawHostName := c.String(sshHostnameFlag)
|
||||
hostname, err := validation.ValidateHostname(rawHostName)
|
||||
if err != nil || rawHostName == "" {
|
||||
url, err := parseURL(rawHostName)
|
||||
if err != nil {
|
||||
log.Err(err).Send()
|
||||
return cli.ShowCommandHelp(c, "ssh")
|
||||
}
|
||||
originURL := ensureURLScheme(hostname)
|
||||
|
||||
// get the headers from the cmdline and add them
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
if c.IsSet(sshTokenSecretFlag) {
|
||||
headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||
}
|
||||
headers.Set("User-Agent", userAgent)
|
||||
|
||||
carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
|
||||
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: originURL,
|
||||
OriginURL: url.String(),
|
||||
Headers: headers,
|
||||
Host: hostname,
|
||||
Host: url.Host,
|
||||
}
|
||||
|
||||
if connectTo := c.String(sshConnectTo); connectTo != "" {
|
||||
@@ -121,16 +130,17 @@ func ssh(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
|
||||
}
|
||||
|
||||
func buildRequestHeaders(values []string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, valuePair := range values {
|
||||
split := strings.Split(valuePair, ":")
|
||||
if len(split) > 1 {
|
||||
headers.Add(strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
|
||||
var s io.ReadWriter
|
||||
s = &carrier.StdinoutStream{}
|
||||
if c.IsSet(sshDebugStream) {
|
||||
maxMessages := c.Uint64(sshDebugStream)
|
||||
if maxMessages == 0 {
|
||||
// default to 10 if provided but unset
|
||||
maxMessages = 10
|
||||
}
|
||||
logger := log.With().Str("host", url.Host).Logger()
|
||||
s = stream.NewDebugStream(s, &logger, maxMessages)
|
||||
}
|
||||
return headers
|
||||
carrier.StartClient(wsConn, s, options)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildRequestHeaders(t *testing.T) {
|
||||
headers := make(http.Header)
|
||||
headers.Add("client", "value")
|
||||
headers.Add("secret", "safe-value")
|
||||
|
||||
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash"})
|
||||
assert.Equal(t, headers.Get("client"), values.Get("client"))
|
||||
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
appURLFlag = "app"
|
||||
loginQuietFlag = "quiet"
|
||||
sshHostnameFlag = "hostname"
|
||||
sshDestinationFlag = "destination"
|
||||
sshURLFlag = "url"
|
||||
@@ -34,6 +36,7 @@ const (
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConnectTo = "connect-to"
|
||||
sshDebugStream = "debug-stream"
|
||||
sshConfigTemplate = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
@@ -81,14 +84,29 @@ func Commands() []*cli.Command {
|
||||
applications from the command line.`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "login",
|
||||
Action: cliutil.Action(login),
|
||||
Usage: "login <url of access application>",
|
||||
Name: "login",
|
||||
Action: cliutil.Action(login),
|
||||
Usage: "login <url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The login subcommand initiates an authentication flow with your identity provider.
|
||||
The subcommand will launch a browser. For headless systems, a url is provided.
|
||||
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
|
||||
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
||||
administrator. cloudflared stores the token in local storage.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: loginQuietFlag,
|
||||
Aliases: []string{"q"},
|
||||
Usage: "do not print the jwt to the command line",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-verbose",
|
||||
Usage: "print only the jwt to stdout",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: appURLFlag,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "curl",
|
||||
@@ -102,12 +120,12 @@ func Commands() []*cli.Command {
|
||||
{
|
||||
Name: "token",
|
||||
Action: cliutil.Action(generateToken),
|
||||
Usage: "token -app=<url of access application>",
|
||||
Usage: "token <url of access application>",
|
||||
ArgsUsage: "url of Access application",
|
||||
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "app",
|
||||
Name: appURLFlag,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,15 +141,18 @@ func Commands() []*cli.Command {
|
||||
Name: sshHostnameFlag,
|
||||
Aliases: []string{"tunnel-host", "T"},
|
||||
Usage: "specify the hostname of your application.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_HOSTNAME"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshDestinationFlag,
|
||||
Usage: "specify the destination address of your SSH server.",
|
||||
Name: sshDestinationFlag,
|
||||
Usage: "specify the destination address of your SSH server.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_DESTINATION"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshURLFlag,
|
||||
Aliases: []string{"listener", "L"},
|
||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_URL"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: sshHeaderFlag,
|
||||
@@ -151,9 +172,12 @@ func Commands() []*cli.Command {
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
Name: logger.LogFileFlag,
|
||||
Usage: "Save application log to this file for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHLevelFlag,
|
||||
@@ -165,6 +189,11 @@ func Commands() []*cli.Command {
|
||||
Hidden: true,
|
||||
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
|
||||
},
|
||||
&cli.Uint64Flag{
|
||||
Name: sshDebugStream,
|
||||
Hidden: true,
|
||||
Usage: "Writes up-to the max provided stream payloads to the logger as debug statements.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -212,10 +241,8 @@ func login(c *cli.Context) error {
|
||||
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
args := c.Args()
|
||||
rawURL := ensureURLScheme(args.First())
|
||||
appURL, err := url.Parse(rawURL)
|
||||
if args.Len() < 1 || err != nil {
|
||||
appURL, err := getAppURLFromArgs(c)
|
||||
if err != nil {
|
||||
log.Error().Msg("Please provide the url of the Access application")
|
||||
return err
|
||||
}
|
||||
@@ -238,21 +265,22 @@ func login(c *cli.Context) error {
|
||||
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
|
||||
return errors.New("empty application token")
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
|
||||
|
||||
if c.Bool(loginQuietFlag) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Chatty by default for backward compat. The new --app flag
|
||||
// is an implicit opt-out of the backwards-compatible chatty output.
|
||||
if c.Bool("no-verbose") || c.IsSet(appURLFlag) {
|
||||
fmt.Fprint(os.Stdout, cfdToken)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureURLScheme prepends a URL with https:// if it doesn't have a scheme. http:// URLs will not be converted.
|
||||
func ensureURLScheme(url string) string {
|
||||
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
url = fmt.Sprintf("https://%s", url)
|
||||
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||
func curl(c *cli.Context) error {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
@@ -280,6 +308,13 @@ func curl(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify that the existing token is still good; if not fetch a new one
|
||||
if err := verifyTokenAtEdge(appURL, appInfo, c, log); err != nil {
|
||||
log.Err(err).Msg("Could not verify token")
|
||||
return err
|
||||
}
|
||||
|
||||
tok, err := token.GetAppTokenIfExists(appInfo)
|
||||
if err != nil || tok == "" {
|
||||
if allowRequest {
|
||||
@@ -320,6 +355,17 @@ func run(cmd string, args ...string) error {
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
func getAppURLFromArgs(c *cli.Context) (*url.URL, error) {
|
||||
var appURLStr string
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
appURLStr = c.String(appURLFlag)
|
||||
} else {
|
||||
appURLStr = args.First()
|
||||
}
|
||||
return parseURL(appURLStr)
|
||||
}
|
||||
|
||||
// token dumps provided token to stdout
|
||||
func generateToken(c *cli.Context) error {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
@@ -329,8 +375,8 @@ func generateToken(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
|
||||
if err != nil || c.NumFlags() < 1 {
|
||||
appURL, err := getAppURLFromArgs(c)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||
return err
|
||||
}
|
||||
@@ -382,7 +428,7 @@ func sshGen(c *cli.Context) error {
|
||||
return cli.ShowCommandHelp(c, "ssh-gen")
|
||||
}
|
||||
|
||||
originURL, err := url.Parse(ensureURLScheme(hostname))
|
||||
originURL, err := parseURL(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -461,6 +507,11 @@ func processURL(s string) (*url.URL, error) {
|
||||
|
||||
// cloudflaredPath pulls the full path of cloudflared on disk
|
||||
func cloudflaredPath() string {
|
||||
path, err := os.Executable()
|
||||
if err == nil && isFileThere(path) {
|
||||
return path
|
||||
}
|
||||
|
||||
for _, p := range strings.Split(os.Getenv("PATH"), ":") {
|
||||
path := fmt.Sprintf("%s/%s", p, "cloudflared")
|
||||
if isFileThere(path) {
|
||||
@@ -483,7 +534,7 @@ func isFileThere(candidate string) bool {
|
||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||
// Returns nil if token is valid.
|
||||
func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
|
||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||
if c.IsSet(sshTokenIDFlag) {
|
||||
headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||
}
|
||||
@@ -518,6 +569,11 @@ func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, err
|
||||
return false, errors.Wrap(err, "Could not create access request")
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("cloudflared_token_check", "true")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
// Do not follow redirects
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package access
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_ensureURLScheme(t *testing.T) {
|
||||
type args struct {
|
||||
url string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{"no scheme", args{"localhost:123"}, "https://localhost:123"},
|
||||
{"http scheme", args{"http://test"}, "https://test"},
|
||||
{"https scheme", args{"https://test"}, "https://test"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ensureURLScheme(tt.args.url); got != tt.want {
|
||||
t.Errorf("ensureURLScheme() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
|
||||
// parseRequestHeaders will take user-provided header values as strings "Content-Type: application/json" and create
|
||||
// a http.Header object.
|
||||
func parseRequestHeaders(values []string) http.Header {
|
||||
headers := make(http.Header)
|
||||
for _, valuePair := range values {
|
||||
header, value, found := strings.Cut(valuePair, ":")
|
||||
if found {
|
||||
headers.Add(strings.TrimSpace(header), strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
// parseHostname will attempt to convert a user provided URL string into a string with some light error checking on
|
||||
// certain expectations from the URL.
|
||||
// Will convert all HTTP URLs to HTTPS
|
||||
func parseURL(input string) (*url.URL, error) {
|
||||
if input == "" {
|
||||
return nil, errors.New("no input provided")
|
||||
}
|
||||
if !strings.HasPrefix(input, "https://") && !strings.HasPrefix(input, "http://") {
|
||||
input = fmt.Sprintf("https://%s", input)
|
||||
}
|
||||
url, err := url.ParseRequestURI(input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse as URL: %w", err)
|
||||
}
|
||||
if url.Scheme != "https" {
|
||||
url.Scheme = "https"
|
||||
}
|
||||
if url.Host == "" {
|
||||
return nil, errors.New("failed to parse Host")
|
||||
}
|
||||
host, err := httpguts.PunycodeHostPort(url.Host)
|
||||
if err != nil || host == "" {
|
||||
return nil, err
|
||||
}
|
||||
if !httpguts.ValidHostHeader(host) {
|
||||
return nil, errors.New("invalid Host provided")
|
||||
}
|
||||
url.Host = host
|
||||
return url, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseRequestHeaders(t *testing.T) {
|
||||
values := parseRequestHeaders([]string{"client: value", "secret: safe-value", "trash", "cf-trace-id: 000:000:0:1:asd"})
|
||||
assert.Len(t, values, 3)
|
||||
assert.Equal(t, "value", values.Get("client"))
|
||||
assert.Equal(t, "safe-value", values.Get("secret"))
|
||||
assert.Equal(t, "000:000:0:1:asd", values.Get("cf-trace-id"))
|
||||
}
|
||||
|
||||
func TestParseURL(t *testing.T) {
|
||||
schemes := []string{
|
||||
"http://",
|
||||
"https://",
|
||||
"",
|
||||
}
|
||||
hosts := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"localhost", "localhost"},
|
||||
{"127.0.0.1", "127.0.0.1"},
|
||||
{"127.0.0.1:9090", "127.0.0.1:9090"},
|
||||
{"::1", "::1"},
|
||||
{"::1:8080", "::1:8080"},
|
||||
{"[::1]", "[::1]"},
|
||||
{"[::1]:8080", "[::1]:8080"},
|
||||
{":8080", ":8080"},
|
||||
{"example.com", "example.com"},
|
||||
{"hello.example.com", "hello.example.com"},
|
||||
{"bücher.example.com", "xn--bcher-kva.example.com"},
|
||||
}
|
||||
paths := []string{
|
||||
"",
|
||||
"/test",
|
||||
"/example.com?qwe=123",
|
||||
}
|
||||
for i, scheme := range schemes {
|
||||
for j, host := range hosts {
|
||||
for k, path := range paths {
|
||||
t.Run(fmt.Sprintf("%d_%d_%d", i, j, k), func(t *testing.T) {
|
||||
input := fmt.Sprintf("%s%s%s", scheme, host.input, path)
|
||||
expected := fmt.Sprintf("%s%s%s", "https://", host.expected, path)
|
||||
url, err := parseURL(input)
|
||||
assert.NoError(t, err, "input: %s\texpected: %s", input, expected)
|
||||
assert.Equal(t, expected, url.String())
|
||||
assert.Equal(t, host.expected, url.Host)
|
||||
assert.Equal(t, "https", url.Scheme)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("no input", func(t *testing.T) {
|
||||
_, err := parseURL("")
|
||||
assert.ErrorContains(t, err, "no input provided")
|
||||
})
|
||||
|
||||
t.Run("missing host", func(t *testing.T) {
|
||||
_, err := parseURL("https:///host")
|
||||
assert.ErrorContains(t, err, "failed to parse Host")
|
||||
})
|
||||
|
||||
t.Run("invalid path only", func(t *testing.T) {
|
||||
_, err := parseURL("/host")
|
||||
assert.ErrorContains(t, err, "failed to parse Host")
|
||||
})
|
||||
|
||||
t.Run("invalid parse URL", func(t *testing.T) {
|
||||
_, err := parseURL("https://host\\host")
|
||||
assert.ErrorContains(t, err, "failed to parse as URL")
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -13,6 +16,7 @@ type BuildInfo struct {
|
||||
GoArch string `json:"go_arch"`
|
||||
BuildType string `json:"build_type"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(buildType, version string) *BuildInfo {
|
||||
@@ -22,11 +26,12 @@ func GetBuildInfo(buildType, version string) *BuildInfo {
|
||||
GoArch: runtime.GOARCH,
|
||||
BuildType: buildType,
|
||||
CloudflaredVersion: version,
|
||||
Checksum: currentBinaryChecksum(),
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(log *zerolog.Logger) {
|
||||
log.Info().Msgf("Version %s", bi.CloudflaredVersion)
|
||||
log.Info().Msgf("Version %s (Checksum %s)", bi.CloudflaredVersion, bi.Checksum)
|
||||
if bi.BuildType != "" {
|
||||
log.Info().Msgf("Built%s", bi.GetBuildTypeMsg())
|
||||
}
|
||||
@@ -51,3 +56,28 @@ func (bi *BuildInfo) GetBuildTypeMsg() string {
|
||||
func (bi *BuildInfo) UserAgent() string {
|
||||
return fmt.Sprintf("cloudflared/%s", bi.CloudflaredVersion)
|
||||
}
|
||||
|
||||
// FileChecksum opens a file and returns the SHA256 checksum.
|
||||
func FileChecksum(filePath string) (string, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func currentBinaryChecksum() string {
|
||||
currentPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum, _ := FileChecksum(currentPath)
|
||||
return sum
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
// +build !windows,!darwin,!linux
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
@@ -25,6 +24,9 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
Name: "install",
|
||||
Usage: "Install cloudflared as a system service",
|
||||
Action: cliutil.ConfiguredAction(installLinuxService),
|
||||
Flags: []cli.Flag{
|
||||
noUpdateServiceFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
@@ -39,19 +41,22 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
// The directory and files that are used by the service.
|
||||
// These are hard-coded in the templates below.
|
||||
const (
|
||||
serviceConfigDir = "/etc/cloudflared"
|
||||
serviceConfigFile = "config.yml"
|
||||
serviceCredentialFile = "cert.pem"
|
||||
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
|
||||
cloudflaredService = "cloudflared.service"
|
||||
serviceConfigDir = "/etc/cloudflared"
|
||||
serviceConfigFile = "config.yml"
|
||||
serviceCredentialFile = "cert.pem"
|
||||
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
|
||||
cloudflaredService = "cloudflared.service"
|
||||
cloudflaredUpdateService = "cloudflared-update.service"
|
||||
cloudflaredUpdateTimer = "cloudflared-update.timer"
|
||||
)
|
||||
|
||||
var systemdTemplates = []ServiceTemplate{
|
||||
{
|
||||
var systemdAllTemplates = map[string]ServiceTemplate{
|
||||
cloudflaredService: {
|
||||
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
|
||||
Content: `[Unit]
|
||||
Description=cloudflared
|
||||
After=network.target
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
TimeoutStartSec=0
|
||||
@@ -64,18 +69,19 @@ RestartSec=5s
|
||||
WantedBy=multi-user.target
|
||||
`,
|
||||
},
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.service",
|
||||
cloudflaredUpdateService: {
|
||||
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateService),
|
||||
Content: `[Unit]
|
||||
Description=Update cloudflared
|
||||
After=network.target
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||
`,
|
||||
},
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.timer",
|
||||
cloudflaredUpdateTimer: {
|
||||
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateTimer),
|
||||
Content: `[Unit]
|
||||
Description=Update cloudflared
|
||||
|
||||
@@ -106,7 +112,7 @@ var sysvTemplate = ServiceTemplate{
|
||||
# Description: cloudflared agent
|
||||
### END INIT INFO
|
||||
name=$(basename $(readlink -f $0))
|
||||
cmd="{{.Path}} --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
cmd="{{.Path}} --pidfile /var/run/$name.pid {{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
pid_file="/var/run/$name.pid"
|
||||
stdout_log="/var/log/$name.log"
|
||||
stderr_log="/var/log/$name.err"
|
||||
@@ -178,6 +184,14 @@ exit 0
|
||||
`,
|
||||
}
|
||||
|
||||
var (
|
||||
noUpdateServiceFlag = &cli.BoolFlag{
|
||||
Name: "no-update-service",
|
||||
Usage: "Disable auto-update of the cloudflared linux service, which restarts the server to upgrade for new versions.",
|
||||
Value: false,
|
||||
}
|
||||
)
|
||||
|
||||
func isSystemd() bool {
|
||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||
return true
|
||||
@@ -196,6 +210,9 @@ func installLinuxService(c *cli.Context) error {
|
||||
Path: etPath,
|
||||
}
|
||||
|
||||
// Check if the "no update flag" is set
|
||||
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
|
||||
|
||||
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
|
||||
if c.NArg() == 0 {
|
||||
extraArgsFunc = buildArgsForConfig
|
||||
@@ -213,10 +230,10 @@ func installLinuxService(c *cli.Context) error {
|
||||
switch {
|
||||
case isSystemd():
|
||||
log.Info().Msgf("Using Systemd")
|
||||
err = installSystemd(&templateArgs, log)
|
||||
err = installSystemd(&templateArgs, autoUpdate, log)
|
||||
default:
|
||||
log.Info().Msgf("Using SysV")
|
||||
err = installSysv(&templateArgs, log)
|
||||
err = installSysv(&templateArgs, autoUpdate, log)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
@@ -261,7 +278,20 @@ credentials-file: CREDENTIALS-FILE
|
||||
}, nil
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error {
|
||||
var systemdTemplates []ServiceTemplate
|
||||
if autoUpdate {
|
||||
systemdTemplates = []ServiceTemplate{
|
||||
systemdAllTemplates[cloudflaredService],
|
||||
systemdAllTemplates[cloudflaredUpdateService],
|
||||
systemdAllTemplates[cloudflaredUpdateTimer],
|
||||
}
|
||||
} else {
|
||||
systemdTemplates = []ServiceTemplate{
|
||||
systemdAllTemplates[cloudflaredService],
|
||||
}
|
||||
}
|
||||
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
err := serviceTemplate.Generate(templateArgs)
|
||||
if err != nil {
|
||||
@@ -273,10 +303,14 @@ func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) erro
|
||||
log.Err(err).Msgf("systemctl enable %s error", cloudflaredService)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
|
||||
log.Err(err).Msg("systemctl start cloudflared-update.timer error")
|
||||
return err
|
||||
|
||||
if autoUpdate {
|
||||
if err := runCommand("systemctl", "start", cloudflaredUpdateTimer); err != nil {
|
||||
log.Err(err).Msgf("systemctl start %s error", cloudflaredUpdateTimer)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := runCommand("systemctl", "daemon-reload"); err != nil {
|
||||
log.Err(err).Msg("systemctl daemon-reload error")
|
||||
return err
|
||||
@@ -284,12 +318,19 @@ func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) erro
|
||||
return runCommand("systemctl", "start", cloudflaredService)
|
||||
}
|
||||
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
func installSysv(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error {
|
||||
confPath, err := sysvTemplate.ResolvePath()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error resolving system path")
|
||||
return err
|
||||
}
|
||||
|
||||
if autoUpdate {
|
||||
templateArgs.ExtraArgs = append([]string{"--autoupdate-freq 24h0m0s"}, templateArgs.ExtraArgs...)
|
||||
} else {
|
||||
templateArgs.ExtraArgs = append([]string{"--no-autoupdate"}, templateArgs.ExtraArgs...)
|
||||
}
|
||||
|
||||
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
||||
log.Err(err).Msg("error generating system template")
|
||||
return err
|
||||
@@ -327,19 +368,35 @@ func uninstallLinuxService(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func uninstallSystemd(log *zerolog.Logger) error {
|
||||
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
|
||||
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
|
||||
return err
|
||||
// Get only the installed services
|
||||
installedServices := make(map[string]ServiceTemplate)
|
||||
for serviceName, serviceTemplate := range systemdAllTemplates {
|
||||
if err := runCommand("systemctl", "list-units", "--all", "|", "grep", serviceName); err == nil {
|
||||
installedServices[serviceName] = serviceTemplate
|
||||
} else {
|
||||
log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName)
|
||||
}
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
|
||||
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
|
||||
return err
|
||||
|
||||
if _, exists := installedServices[cloudflaredService]; exists {
|
||||
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
|
||||
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
|
||||
return err
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
|
||||
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
|
||||
log.Err(err).Msg("systemctl stop cloudflared-update.timer error")
|
||||
return err
|
||||
|
||||
if _, exists := installedServices[cloudflaredUpdateTimer]; exists {
|
||||
if err := runCommand("systemctl", "stop", cloudflaredUpdateTimer); err != nil {
|
||||
log.Err(err).Msgf("systemctl stop %s error", cloudflaredUpdateTimer)
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
|
||||
for _, serviceTemplate := range installedServices {
|
||||
if err := serviceTemplate.Remove(); err != nil {
|
||||
log.Err(err).Msg("error removing service template")
|
||||
return err
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package main
|
||||
|
||||
|
||||
+16
-1
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -49,6 +50,9 @@ var (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// FIXME: TUN-8148: Disable QUIC_GO ECN due to bugs in proper detection if supported
|
||||
os.Setenv("QUIC_GO_DISABLE_ECN", "1")
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||
maxprocs.Set()
|
||||
@@ -87,7 +91,7 @@ func main() {
|
||||
|
||||
tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(graceShutdownC, Version)
|
||||
updater.Init(Version)
|
||||
updater.Init(bInfo)
|
||||
tracing.Init(Version)
|
||||
token.Init(Version)
|
||||
tail.Init(bInfo)
|
||||
@@ -130,11 +134,22 @@ To determine if an update happened in a script, check for error code 11.`,
|
||||
{
|
||||
Name: "version",
|
||||
Action: func(c *cli.Context) (err error) {
|
||||
if c.Bool("short") {
|
||||
fmt.Println(strings.Split(c.App.Version, " ")[0])
|
||||
return nil
|
||||
}
|
||||
version(c)
|
||||
return nil
|
||||
},
|
||||
Usage: versionText,
|
||||
Description: versionText,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "short",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "print just the version number",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -64,7 +63,7 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
|
||||
return fmt.Errorf("error creating %s: %v", plistFolder, err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(resolvedPath, buffer.Bytes(), fileMode)
|
||||
err = os.WriteFile(resolvedPath, buffer.Bytes(), fileMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing %s: %v", resolvedPath, err)
|
||||
}
|
||||
@@ -103,7 +102,7 @@ func runCommand(command string, args ...string) error {
|
||||
return fmt.Errorf("error starting %s: %v", command, err)
|
||||
}
|
||||
|
||||
output, _ := ioutil.ReadAll(stderr)
|
||||
output, _ := io.ReadAll(stderr)
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %v returned with error code %v due to: %v", command, args, err, string(output))
|
||||
|
||||
@@ -32,6 +32,38 @@ func Init(bi *cliutil.BuildInfo) {
|
||||
}
|
||||
|
||||
func Command() *cli.Command {
|
||||
subcommands := []*cli.Command{
|
||||
buildTailManagementTokenSubcommand(),
|
||||
}
|
||||
|
||||
return buildTailCommand(subcommands)
|
||||
}
|
||||
|
||||
func buildTailManagementTokenSubcommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "token",
|
||||
Action: cliutil.ConfiguredAction(managementTokenCommand),
|
||||
Usage: "Get management access jwt",
|
||||
UsageText: "cloudflared tail token TUNNEL_ID",
|
||||
Description: `Get management access jwt for a tunnel`,
|
||||
Hidden: true,
|
||||
}
|
||||
}
|
||||
|
||||
func managementTokenCommand(c *cli.Context) error {
|
||||
log := createLogger(c)
|
||||
token, err := getManagementToken(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var tokenResponse = struct {
|
||||
Token string `json:"token"`
|
||||
}{Token: token}
|
||||
|
||||
return json.NewEncoder(os.Stdout).Encode(tokenResponse)
|
||||
}
|
||||
|
||||
func buildTailCommand(subcommands []*cli.Command) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "tail",
|
||||
Action: Run,
|
||||
@@ -51,16 +83,28 @@ func Command() *cli.Command {
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "level",
|
||||
Usage: "Filter by specific log levels (debug, info, warn, error)",
|
||||
Usage: "Filter by specific log levels (debug, info, warn, error). Filters by debug log level by default.",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_FILTER_LEVEL"},
|
||||
Value: "debug",
|
||||
},
|
||||
&cli.Float64Flag{
|
||||
Name: "sample",
|
||||
Usage: "Sample log events by percentage (0.0 .. 1.0). No sampling by default.",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_FILTER_SAMPLE"},
|
||||
Value: 1.0,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Usage: "Access token for a specific tunnel",
|
||||
Value: "",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_TOKEN"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Usage: "Output format for the logs (default, json)",
|
||||
Value: "default",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_OUTPUT"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "management-hostname",
|
||||
Usage: "Management hostname to signify incoming management requests",
|
||||
@@ -87,6 +131,7 @@ func Command() *cli.Command {
|
||||
Value: credentials.FindDefaultOriginCertPath(),
|
||||
},
|
||||
},
|
||||
Subcommands: subcommands,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +184,11 @@ func createLogger(c *cli.Context) *zerolog.Logger {
|
||||
func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
|
||||
var level *management.LogLevel
|
||||
var events []management.LogEventType
|
||||
var sample float64
|
||||
|
||||
argLevel := c.String("level")
|
||||
argEvents := c.StringSlice("event")
|
||||
argSample := c.Float64("sample")
|
||||
|
||||
if argLevel != "" {
|
||||
l, ok := management.ParseLogLevel(argLevel)
|
||||
@@ -159,14 +206,20 @@ func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
|
||||
events = append(events, t)
|
||||
}
|
||||
|
||||
if level == nil && len(events) == 0 {
|
||||
if argSample <= 0.0 || argSample > 1.0 {
|
||||
return nil, fmt.Errorf("invalid --sample value provided, please make sure it is in the range (0.0 .. 1.0)")
|
||||
}
|
||||
sample = argSample
|
||||
|
||||
if level == nil && len(events) == 0 && argSample != 1.0 {
|
||||
// When no filters are provided, do not return a StreamingFilters struct
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &management.StreamingFilters{
|
||||
Level: level,
|
||||
Events: events,
|
||||
Level: level,
|
||||
Events: events,
|
||||
Sampling: sample,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -223,6 +276,24 @@ func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
|
||||
return url.URL{Scheme: "wss", Host: managementHostname, Path: "/logs", RawQuery: query.Encode()}, nil
|
||||
}
|
||||
|
||||
func printLine(log *management.Log, logger *zerolog.Logger) {
|
||||
fields, err := json.Marshal(log.Fields)
|
||||
if err != nil {
|
||||
fields = []byte("unable to parse fields")
|
||||
logger.Debug().Msgf("unable to parse fields from event %+v", log)
|
||||
}
|
||||
fmt.Printf("%s %s %s %s %s\n", log.Time, log.Level, log.Event, log.Message, fields)
|
||||
}
|
||||
|
||||
func printJSON(log *management.Log, logger *zerolog.Logger) {
|
||||
output, err := json.Marshal(log)
|
||||
if err != nil {
|
||||
logger.Debug().Msgf("unable to parse event to json %+v", log)
|
||||
} else {
|
||||
fmt.Println(string(output))
|
||||
}
|
||||
}
|
||||
|
||||
// Run implements a foreground runner
|
||||
func Run(c *cli.Context) error {
|
||||
log := createLogger(c)
|
||||
@@ -231,6 +302,16 @@ func Run(c *cli.Context) error {
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
output := "default"
|
||||
switch c.String("output") {
|
||||
case "default", "":
|
||||
output = "default"
|
||||
case "json":
|
||||
output = "json"
|
||||
default:
|
||||
log.Err(errors.New("invalid --output value provided, please make sure it is one of: default, json")).Send()
|
||||
}
|
||||
|
||||
filters, err := parseFilters(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("invalid filters provided")
|
||||
@@ -311,12 +392,11 @@ func Run(c *cli.Context) error {
|
||||
}
|
||||
// Output all the logs received to stdout
|
||||
for _, l := range logs.Logs {
|
||||
fields, err := json.Marshal(l.Fields)
|
||||
if err != nil {
|
||||
fields = []byte("unable to parse fields")
|
||||
log.Debug().Msgf("unable to parse fields from event %+v", l)
|
||||
if output == "json" {
|
||||
printJSON(l, log)
|
||||
} else {
|
||||
printLine(l, log)
|
||||
}
|
||||
fmt.Printf("%s %s %s %s %s\n", l.Time, l.Level, l.Event, l.Message, fields)
|
||||
}
|
||||
case management.UnknownServerEventType:
|
||||
fallthrough
|
||||
|
||||
+222
-19
@@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime/trace"
|
||||
@@ -12,7 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/coreos/go-systemd/v22/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/google/uuid"
|
||||
@@ -29,6 +28,8 @@ import (
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
"github.com/cloudflare/cloudflared/diagnostic"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/tunnelstate"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
@@ -78,6 +80,25 @@ const (
|
||||
// hostKeyPath is the path of the dir to save SSH host keys too
|
||||
hostKeyPath = "host-key-path"
|
||||
|
||||
// rpcTimeout is how long to wait for a Capnp RPC request to the edge
|
||||
rpcTimeout = "rpc-timeout"
|
||||
|
||||
// writeStreamTimeout sets if we should have a timeout when writing data to a stream towards the destination (edge/origin).
|
||||
writeStreamTimeout = "write-stream-timeout"
|
||||
|
||||
// quicDisablePathMTUDiscovery sets if QUIC should not perform PTMU discovery and use a smaller (safe) packet size.
|
||||
// Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||
// Note that this may result in packet drops for UDP proxying, since we expect being able to send at least 1280 bytes of inner packets.
|
||||
quicDisablePathMTUDiscovery = "quic-disable-pmtu-discovery"
|
||||
|
||||
// quicConnLevelFlowControlLimit controls the max flow control limit allocated for a QUIC connection. This controls how much data is the
|
||||
// receiver willing to buffer. Once the limit is reached, the sender will send a DATA_BLOCKED frame to indicate it has more data to write,
|
||||
// but it's blocked by flow control
|
||||
quicConnLevelFlowControlLimit = "quic-connection-level-flow-control-limit"
|
||||
// quicStreamLevelFlowControlLimit is similar to quicConnLevelFlowControlLimit but for each QUIC stream. When the sender is blocked,
|
||||
// it will send a STREAM_DATA_BLOCKED frame
|
||||
quicStreamLevelFlowControlLimit = "quic-stream-level-flow-control-limit"
|
||||
|
||||
// uiFlag is to enable launching cloudflared in interactive UI mode
|
||||
uiFlag = "ui"
|
||||
|
||||
@@ -95,6 +116,7 @@ Eg. cloudflared tunnel --url localhost:8080/.
|
||||
Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes.
|
||||
For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)
|
||||
`
|
||||
connectorLabelFlag = "label"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -105,6 +127,94 @@ var (
|
||||
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
|
||||
"any existing DNS records for this hostname.", overwriteDNSFlag)
|
||||
deprecatedClassicTunnelErr = fmt.Errorf("Classic tunnels have been deprecated, please use Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)")
|
||||
// TODO: TUN-8756 the list below denotes the flags that do not possess any kind of sensitive information
|
||||
// however this approach is not maintainble in the long-term.
|
||||
nonSecretFlagsList = []string{
|
||||
"config",
|
||||
"autoupdate-freq",
|
||||
"no-autoupdate",
|
||||
"metrics",
|
||||
"pidfile",
|
||||
"url",
|
||||
"hello-world",
|
||||
"socks5",
|
||||
"proxy-connect-timeout",
|
||||
"proxy-tls-timeout",
|
||||
"proxy-tcp-keepalive",
|
||||
"proxy-no-happy-eyeballs",
|
||||
"proxy-keepalive-connections",
|
||||
"proxy-keepalive-timeout",
|
||||
"proxy-connection-timeout",
|
||||
"proxy-expect-continue-timeout",
|
||||
"http-host-header",
|
||||
"origin-server-name",
|
||||
"unix-socket",
|
||||
"origin-ca-pool",
|
||||
"no-tls-verify",
|
||||
"no-chunked-encoding",
|
||||
"http2-origin",
|
||||
"management-hostname",
|
||||
"service-op-ip",
|
||||
"local-ssh-port",
|
||||
"ssh-idle-timeout",
|
||||
"ssh-max-timeout",
|
||||
"bucket-name",
|
||||
"region-name",
|
||||
"s3-url-host",
|
||||
"host-key-path",
|
||||
"ssh-server",
|
||||
"bastion",
|
||||
"proxy-address",
|
||||
"proxy-port",
|
||||
"loglevel",
|
||||
"transport-loglevel",
|
||||
"logfile",
|
||||
"log-directory",
|
||||
"trace-output",
|
||||
"proxy-dns",
|
||||
"proxy-dns-port",
|
||||
"proxy-dns-address",
|
||||
"proxy-dns-upstream",
|
||||
"proxy-dns-max-upstream-conns",
|
||||
"proxy-dns-bootstrap",
|
||||
"is-autoupdated",
|
||||
"edge",
|
||||
"region",
|
||||
"edge-ip-version",
|
||||
"edge-bind-address",
|
||||
"cacert",
|
||||
"hostname",
|
||||
"id",
|
||||
"lb-pool",
|
||||
"api-url",
|
||||
"metrics-update-freq",
|
||||
"tag",
|
||||
"heartbeat-interval",
|
||||
"heartbeat-count",
|
||||
"max-edge-addr-retries",
|
||||
"retries",
|
||||
"ha-connections",
|
||||
"rpc-timeout",
|
||||
"write-stream-timeout",
|
||||
"quic-disable-pmtu-discovery",
|
||||
"quic-connection-level-flow-control-limit",
|
||||
"quic-stream-level-flow-control-limit",
|
||||
"label",
|
||||
"grace-period",
|
||||
"compression-quality",
|
||||
"use-reconnect-token",
|
||||
"dial-edge-timeout",
|
||||
"stdin-control",
|
||||
"name",
|
||||
"ui",
|
||||
"quick-service",
|
||||
"max-fetch-size",
|
||||
"post-quantum",
|
||||
"management-diagnostics",
|
||||
"protocol",
|
||||
"overwrite-dns",
|
||||
"help",
|
||||
}
|
||||
)
|
||||
|
||||
func Flags() []cli.Flag {
|
||||
@@ -119,6 +229,7 @@ func Commands() []*cli.Command {
|
||||
buildVirtualNetworkSubcommand(false),
|
||||
buildRunCommand(),
|
||||
buildListCommand(),
|
||||
buildReadyCommand(),
|
||||
buildInfoCommand(),
|
||||
buildIngressSubcommand(),
|
||||
buildDeleteCommand(),
|
||||
@@ -275,7 +386,7 @@ func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
|
||||
func StartServer(
|
||||
c *cli.Context,
|
||||
info *cliutil.BuildInfo,
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
namedTunnel *connection.TunnelProperties,
|
||||
log *zerolog.Logger,
|
||||
) error {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
@@ -295,7 +406,7 @@ func StartServer(
|
||||
}
|
||||
|
||||
if c.IsSet("trace-output") {
|
||||
tmpTraceFile, err := ioutil.TempFile("", "trace")
|
||||
tmpTraceFile, err := os.CreateTemp("", "trace")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to create new temporary file to save trace output")
|
||||
}
|
||||
@@ -331,7 +442,7 @@ func StartServer(
|
||||
logClientOptions(c, log)
|
||||
|
||||
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(c.Context)
|
||||
defer cancel()
|
||||
|
||||
go waitForSignal(graceShutdownC, log)
|
||||
@@ -383,7 +494,7 @@ func StartServer(
|
||||
observer.SendURL(quickTunnelURL)
|
||||
}
|
||||
|
||||
tunnelConfig, orchestratorConfig, err := prepareTunnelConfig(c, info, log, logTransport, observer, namedTunnel)
|
||||
tunnelConfig, orchestratorConfig, err := prepareTunnelConfig(ctx, c, info, log, logTransport, observer, namedTunnel)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't start tunnel")
|
||||
return err
|
||||
@@ -397,29 +508,64 @@ func StartServer(
|
||||
}
|
||||
}
|
||||
|
||||
localRules := []ingress.Rule{}
|
||||
if features.Contains(features.FeatureManagementLogs) {
|
||||
mgmt := management.New(c.String("management-hostname"), logger.ManagementLogger.Log, logger.ManagementLogger)
|
||||
localRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
|
||||
// Disable ICMP packet routing for quick tunnels
|
||||
if quickTunnelURL != "" {
|
||||
tunnelConfig.ICMPRouterServer = nil
|
||||
}
|
||||
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, localRules, tunnelConfig.Log)
|
||||
|
||||
internalRules := []ingress.Rule{}
|
||||
if features.Contains(features.FeatureManagementLogs) {
|
||||
serviceIP := c.String("service-op-ip")
|
||||
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
|
||||
if serviceAddr, err := edgeAddrs.GetAddrForRPC(); err == nil {
|
||||
serviceIP = serviceAddr.TCP.String()
|
||||
}
|
||||
}
|
||||
|
||||
mgmt := management.New(
|
||||
c.String("management-hostname"),
|
||||
c.Bool("management-diagnostics"),
|
||||
serviceIP,
|
||||
clientID,
|
||||
c.String(connectorLabelFlag),
|
||||
logger.ManagementLogger.Log,
|
||||
logger.ManagementLogger,
|
||||
)
|
||||
internalRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
|
||||
}
|
||||
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
|
||||
metricsListener, err := metrics.CreateMetricsListener(&listeners, c.String("metrics"))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error opening metrics server listener")
|
||||
return errors.Wrap(err, "Error opening metrics server listener")
|
||||
}
|
||||
|
||||
defer metricsListener.Close()
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
readinessServer := metrics.NewReadyServer(log, clientID)
|
||||
observer.RegisterSink(readinessServer)
|
||||
tracker := tunnelstate.NewConnTracker(log)
|
||||
observer.RegisterSink(tracker)
|
||||
|
||||
readinessServer := metrics.NewReadyServer(clientID, tracker)
|
||||
diagnosticHandler := diagnostic.NewDiagnosticHandler(
|
||||
log,
|
||||
0,
|
||||
diagnostic.NewSystemCollectorImpl(buildInfo.CloudflaredVersion),
|
||||
tunnelConfig.NamedTunnel.Credentials.TunnelID,
|
||||
clientID,
|
||||
tracker,
|
||||
c,
|
||||
nonSecretFlagsList,
|
||||
)
|
||||
metricsConfig := metrics.Config{
|
||||
ReadyServer: readinessServer,
|
||||
DiagnosticHandler: diagnosticHandler,
|
||||
QuickTunnelHostname: quickTunnelURL,
|
||||
Orchestrator: orchestrator,
|
||||
}
|
||||
@@ -631,9 +777,9 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Usage: "Custom tags used to identify this tunnel, in format `KEY=VALUE`. Multiple tags may be specified",
|
||||
Usage: "Custom tags used to identify this tunnel via added HTTP request headers to the origin, in format `KEY=VALUE`. Multiple tags may be specified.",
|
||||
EnvVars: []string{"TUNNEL_TAG"},
|
||||
Hidden: shouldHide,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "heartbeat-interval",
|
||||
@@ -667,6 +813,44 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Value: 4,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: rpcTimeout,
|
||||
Value: 5 * time.Second,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: writeStreamTimeout,
|
||||
EnvVars: []string{"TUNNEL_STREAM_WRITE_TIMEOUT"},
|
||||
Usage: "Use this option to add a stream write timeout for connections when writing towards the origin or edge. Default is 0 which disables the write timeout.",
|
||||
Value: 0 * time.Second,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: quicDisablePathMTUDiscovery,
|
||||
EnvVars: []string{"TUNNEL_DISABLE_QUIC_PMTU"},
|
||||
Usage: "Use this option to disable PTMU discovery for QUIC connections. This will result in lower packet sizes. Not however, that this may cause instability for UDP proxying.",
|
||||
Value: false,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: quicConnLevelFlowControlLimit,
|
||||
EnvVars: []string{"TUNNEL_QUIC_CONN_LEVEL_FLOW_CONTROL_LIMIT"},
|
||||
Usage: "Use this option to change the connection-level flow control limit for QUIC transport.",
|
||||
Value: 30 * (1 << 20), // 30 MB
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: quicStreamLevelFlowControlLimit,
|
||||
EnvVars: []string{"TUNNEL_QUIC_STREAM_LEVEL_FLOW_CONTROL_LIMIT"},
|
||||
Usage: "Use this option to change the connection-level flow control limit for QUIC transport.",
|
||||
Value: 6 * (1 << 20), // 6 MB
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: connectorLabelFlag,
|
||||
Usage: "Use this option to give a meaningful label to a specific connector. When a tunnel starts up, a connector id unique to the tunnel is generated. This is a uuid. To make it easier to identify a connector, we will use the hostname of the machine the tunnel is running on along with the connector ID. This option exists if one wants to have more control over what their individual connectors are called.",
|
||||
Value: "",
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: "grace-period",
|
||||
Usage: "When cloudflared receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shutdown. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received.",
|
||||
@@ -735,6 +919,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_POST_QUANTUM"},
|
||||
Hidden: FipsEnabled,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "management-diagnostics",
|
||||
Usage: "Enables the in-depth diagnostic routes to be made available over the management service (/debug/pprof, /metrics, etc.)",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_DIAGNOSTICS"},
|
||||
Value: true,
|
||||
}),
|
||||
selectProtocolFlag,
|
||||
overwriteDNSFlag,
|
||||
}...)
|
||||
@@ -772,9 +962,15 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "metrics",
|
||||
Value: "localhost:",
|
||||
Usage: "Listen address for metrics reporting.",
|
||||
Name: "metrics",
|
||||
Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
|
||||
Usage: fmt.Sprintf(
|
||||
`Listen address for metrics reporting. If no address is passed cloudflared will try to bind to %v.
|
||||
If all are unavailable, a random port will be used. Note that when running cloudflared from an virtual
|
||||
environment the default address binds to all interfaces, hence, it is important to isolate the host
|
||||
and virtualized host network stacks from each other`,
|
||||
metrics.GetMetricsKnownAddresses(metrics.Runtime),
|
||||
),
|
||||
EnvVars: []string{"TUNNEL_METRICS"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
@@ -907,6 +1103,13 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: true,
|
||||
Value: "management.argotunnel.com",
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "service-op-ip",
|
||||
Usage: "Fallback IP for service operations run by the management service.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_OP_IP"},
|
||||
Hidden: true,
|
||||
Value: "198.41.200.113:80",
|
||||
}),
|
||||
}
|
||||
return append(flags, sshFlags(shouldHide)...)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
)
|
||||
|
||||
func TestDedup(t *testing.T) {
|
||||
expected := []string{"a", "b"}
|
||||
actual := dedup([]string{"a", "b", "a"})
|
||||
actual := features.Dedup([]string{"a", "b", "a"})
|
||||
require.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
mathRand "math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
@@ -27,15 +27,18 @@ import (
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const secretValue = "*****"
|
||||
const (
|
||||
secretValue = "*****"
|
||||
icmpFunnelTimeout = time.Second * 10
|
||||
)
|
||||
|
||||
var (
|
||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
developerPortal = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup"
|
||||
serviceUrl = developerPortal + "/tunnel-guide/local/as-a-service/"
|
||||
argumentsUrl = developerPortal + "/tunnel-guide/local/local-management/arguments/"
|
||||
|
||||
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
|
||||
|
||||
@@ -105,7 +108,7 @@ func isSecretEnvVar(key string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelProperties) bool {
|
||||
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.TunnelProperties) bool {
|
||||
return c.IsSet("proxy-dns") &&
|
||||
!(c.IsSet("name") || // adhoc-named tunnel
|
||||
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
|
||||
@@ -113,11 +116,12 @@ func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelPrope
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
ctx context.Context,
|
||||
c *cli.Context,
|
||||
info *cliutil.BuildInfo,
|
||||
log, logTransport *zerolog.Logger,
|
||||
observer *connection.Observer,
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
namedTunnel *connection.TunnelProperties,
|
||||
) (*supervisor.TunnelConfig, *orchestration.Config, error) {
|
||||
clientID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
@@ -129,26 +133,40 @@ func prepareTunnelConfig(
|
||||
log.Err(err).Msg("Tag parse failure")
|
||||
return nil, nil, errors.Wrap(err, "Tag parse failure")
|
||||
}
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID.String()})
|
||||
tags = append(tags, pogs.Tag{Name: "ID", Value: clientID.String()})
|
||||
|
||||
transportProtocol := c.String("protocol")
|
||||
needPQ := c.Bool("post-quantum")
|
||||
if needPQ {
|
||||
|
||||
clientFeatures := features.Dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
|
||||
|
||||
staticFeatures := features.StaticFeatures{}
|
||||
if c.Bool("post-quantum") {
|
||||
if FipsEnabled {
|
||||
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode")
|
||||
}
|
||||
pqMode := features.PostQuantumStrict
|
||||
staticFeatures.PostQuantumMode = &pqMode
|
||||
}
|
||||
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, staticFeatures, log)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
|
||||
}
|
||||
pqMode := featureSelector.PostQuantumMode()
|
||||
if pqMode == features.PostQuantumStrict {
|
||||
// Error if the user tries to force a non-quic transport protocol
|
||||
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
|
||||
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
|
||||
}
|
||||
transportProtocol = connection.QUIC.String()
|
||||
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
|
||||
|
||||
log.Info().Msgf(
|
||||
"Using hybrid post-quantum key agreement %s",
|
||||
supervisor.PQKexName,
|
||||
)
|
||||
}
|
||||
|
||||
clientFeatures := dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
|
||||
if needPQ {
|
||||
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
|
||||
}
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
namedTunnel.Client = pogs.ClientInfo{
|
||||
ClientID: clientID[:],
|
||||
Features: clientFeatures,
|
||||
Version: info.Version(),
|
||||
@@ -203,15 +221,6 @@ func prepareTunnelConfig(
|
||||
log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version")
|
||||
}
|
||||
|
||||
var pqKexIdx int
|
||||
if needPQ {
|
||||
pqKexIdx = mathRand.Intn(len(supervisor.PQKexes))
|
||||
log.Info().Msgf(
|
||||
"Using experimental hybrid post-quantum key agreement %s",
|
||||
supervisor.PQKexNames[supervisor.PQKexes[pqKexIdx]],
|
||||
)
|
||||
}
|
||||
|
||||
tunnelConfig := &supervisor.TunnelConfig{
|
||||
GracePeriod: gracePeriod,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
@@ -222,7 +231,6 @@ func prepareTunnelConfig(
|
||||
EdgeIPVersion: edgeIPVersion,
|
||||
EdgeBindAddr: edgeBindAddr,
|
||||
HAConnections: c.Int(haConnectionsFlag),
|
||||
IncidentLookup: supervisor.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
@@ -231,25 +239,30 @@ func prepareTunnelConfig(
|
||||
Observer: observer,
|
||||
ReportedVersion: info.Version(),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NamedTunnel: namedTunnel,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
NeedPQ: needPQ,
|
||||
PQKexIdx: pqKexIdx,
|
||||
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
NamedTunnel: namedTunnel,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
FeatureSelector: featureSelector,
|
||||
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
|
||||
RPCTimeout: c.Duration(rpcTimeout),
|
||||
WriteStreamTimeout: c.Duration(writeStreamTimeout),
|
||||
DisableQUICPathMTUDiscovery: c.Bool(quicDisablePathMTUDiscovery),
|
||||
QUICConnectionLevelFlowControlLimit: c.Uint64(quicConnLevelFlowControlLimit),
|
||||
QUICStreamLevelFlowControlLimit: c.Uint64(quicStreamLevelFlowControlLimit),
|
||||
}
|
||||
packetConfig, err := newPacketConfig(c, log)
|
||||
icmpRouter, err := newICMPRouter(c, log)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("ICMP proxy feature is disabled")
|
||||
} else {
|
||||
tunnelConfig.PacketConfig = packetConfig
|
||||
tunnelConfig.ICMPRouterServer = icmpRouter
|
||||
}
|
||||
orchestratorConfig := &orchestration.Config{
|
||||
Ingress: &ingressRules,
|
||||
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
|
||||
ConfigurationFlags: parseConfigFlags(c),
|
||||
WriteTimeout: c.Duration(writeStreamTimeout),
|
||||
}
|
||||
return tunnelConfig, orchestratorConfig, nil
|
||||
}
|
||||
@@ -275,26 +288,7 @@ func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// Remove any duplicates from the slice
|
||||
func dedup(slice []string) []string {
|
||||
|
||||
// Convert the slice into a set
|
||||
set := make(map[string]bool, 0)
|
||||
for _, str := range slice {
|
||||
set[str] = true
|
||||
}
|
||||
|
||||
// Convert the set back into a slice
|
||||
keys := make([]string, len(set))
|
||||
i := 0
|
||||
for str := range set {
|
||||
keys[i] = str
|
||||
i++
|
||||
}
|
||||
return keys
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// ParseConfigIPVersion returns the IP version from possible expected values from config
|
||||
@@ -357,7 +351,7 @@ func adjustIPVersionByBindAddress(ipVersion allregions.ConfigIPVersion, ip net.I
|
||||
}
|
||||
}
|
||||
|
||||
func newPacketConfig(c *cli.Context, logger *zerolog.Logger) (*ingress.GlobalRouterConfig, error) {
|
||||
func newICMPRouter(c *cli.Context, logger *zerolog.Logger) (ingress.ICMPRouterServer, error) {
|
||||
ipv4Src, err := determineICMPv4Src(c.String("icmpv4-src"), logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to determine IPv4 source address for ICMP proxy")
|
||||
@@ -374,16 +368,11 @@ func newPacketConfig(c *cli.Context, logger *zerolog.Logger) (*ingress.GlobalRou
|
||||
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
|
||||
}
|
||||
|
||||
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, zone, logger)
|
||||
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, logger, icmpFunnelTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ingress.GlobalRouterConfig{
|
||||
ICMPRouter: icmpRouter,
|
||||
IPv4Src: ipv4Src,
|
||||
IPv6Src: ipv6Src,
|
||||
Zone: zone,
|
||||
}, nil
|
||||
return icmpRouter, nil
|
||||
}
|
||||
|
||||
func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) {
|
||||
@@ -413,13 +402,12 @@ type interfaceIP struct {
|
||||
|
||||
func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) {
|
||||
if userDefinedSrc != "" {
|
||||
userDefinedIP, zone, _ := strings.Cut(userDefinedSrc, "%")
|
||||
addr, err := netip.ParseAddr(userDefinedIP)
|
||||
addr, err := netip.ParseAddr(userDefinedSrc)
|
||||
if err != nil {
|
||||
return netip.Addr{}, "", err
|
||||
}
|
||||
if addr.Is6() {
|
||||
return addr, zone, nil
|
||||
return addr, addr.Zone(), nil
|
||||
}
|
||||
return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -23,5 +22,5 @@ func (fs realFileSystem) validFilePath(path string) bool {
|
||||
}
|
||||
|
||||
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
|
||||
return ioutil.ReadFile(filePath)
|
||||
return os.ReadFile(filePath)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func testURLCommand(c *cli.Context) error {
|
||||
}
|
||||
|
||||
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
|
||||
fmt.Printf("Matched rule #%d\n", i+1)
|
||||
fmt.Printf("Matched rule #%d\n", i)
|
||||
fmt.Println(ing.Rules[i].MultiLineString())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -53,6 +52,7 @@ func login(c *cli.Context) error {
|
||||
|
||||
resourceData, err := token.RunTransfer(
|
||||
loginURL,
|
||||
"",
|
||||
"cert",
|
||||
"callback",
|
||||
callbackStoreURL,
|
||||
@@ -65,7 +65,7 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
||||
if err := os.WriteFile(path, resourceData, 0600); err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package tunnel
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -15,10 +16,7 @@ import (
|
||||
|
||||
const httpTimeout = 15 * time.Second
|
||||
|
||||
const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to" +
|
||||
" experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you " +
|
||||
"intend to use Tunnels in production you should use a pre-created named tunnel by following: " +
|
||||
"https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"
|
||||
const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee, are subject to the Cloudflare Online Services Terms of Use (https://www.cloudflare.com/website-terms/), and Cloudflare reserves the right to investigate your use of Tunnels for violations of such terms. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"
|
||||
|
||||
// RunQuickTunnel requests a tunnel from the specified service.
|
||||
// We use this to power quick tunnels on trycloudflare.com, but the
|
||||
@@ -35,14 +33,29 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
|
||||
resp, err := client.Post(fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), "application/json", nil)
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build quick tunnel request")
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("User-Agent", buildInfo.UserAgent())
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// This will read the entire response into memory so we can print it in case of error
|
||||
rsp_body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read quick-tunnel response")
|
||||
}
|
||||
|
||||
var data QuickTunnelResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
if err := json.Unmarshal(rsp_body, &data); err != nil {
|
||||
rsp_string := string(rsp_body)
|
||||
fields := map[string]interface{}{"status_code": resp.Status}
|
||||
sc.log.Err(err).Fields(fields).Msgf("Error unmarshaling QuickTunnel response: %s", rsp_string)
|
||||
return errors.Wrap(err, "failed to unmarshal quick Tunnel")
|
||||
}
|
||||
|
||||
@@ -79,7 +92,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
&connection.TunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
sc.log,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package tunnel
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||
var errorLines []string
|
||||
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
|
||||
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
|
||||
if deleteErr := client.DeleteTunnel(tunnel.ID, true); deleteErr != nil {
|
||||
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
|
||||
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
|
||||
} else {
|
||||
@@ -206,13 +206,8 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
if !tunnel.DeletedAt.IsZero() {
|
||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||
}
|
||||
if forceFlagSet {
|
||||
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.DeleteTunnel(tunnel.ID); err != nil {
|
||||
if err := client.DeleteTunnel(tunnel.ID, forceFlagSet); err != nil {
|
||||
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
|
||||
}
|
||||
|
||||
@@ -266,7 +261,7 @@ func (sc *subcommandContext) runWithCredentials(credentials connection.Credentia
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelProperties{Credentials: credentials},
|
||||
&connection.TunnelProperties{Credentials: credentials},
|
||||
sc.log,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
@@ -24,12 +27,12 @@ func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, err
|
||||
return client.AddRoute(newRoute)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
|
||||
func (sc *subcommandContext) deleteRoute(id uuid.UUID) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.DeleteRoute(params)
|
||||
return client.DeleteRoute(id)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
||||
@@ -39,3 +42,25 @@ func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfap
|
||||
}
|
||||
return client.GetByIP(params)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getRouteId(network net.IPNet, vnetId *uuid.UUID) (uuid.UUID, error) {
|
||||
filters := cfapi.NewIPRouteFilter()
|
||||
filters.NotDeleted()
|
||||
filters.NetworkIsSubsetOf(network)
|
||||
filters.NetworkIsSupersetOf(network)
|
||||
|
||||
if vnetId != nil {
|
||||
filters.VNetID(*vnetId)
|
||||
}
|
||||
|
||||
result, err := sc.listRoutes(filters)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
if len(result) != 1 {
|
||||
return uuid.Nil, errors.New("unable to find route for provided network and vnet")
|
||||
}
|
||||
|
||||
return result[0].ID, nil
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, erro
|
||||
return "token", nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -120,8 +121,8 @@ var (
|
||||
forceDeleteFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Cleans up any stale connections before the tunnel is deleted. cloudflared will not " +
|
||||
"delete a tunnel with connections without this flag.",
|
||||
Usage: "Deletes a tunnel even if tunnel is connected and it has dependencies associated to it. (eg. IP routes)." +
|
||||
" It is not possible to delete tunnels that have connections or non-deleted dependencies, without this flag.",
|
||||
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
|
||||
}
|
||||
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
@@ -241,7 +242,7 @@ func writeTunnelCredentials(filePath string, credentials *connection.Credentials
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
||||
}
|
||||
return ioutil.WriteFile(filePath, body, 400)
|
||||
return os.WriteFile(filePath, body, 0400)
|
||||
}
|
||||
|
||||
func buildListCommand() *cli.Command {
|
||||
@@ -398,6 +399,39 @@ func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected boo
|
||||
return strings.Join(output, ", ")
|
||||
}
|
||||
|
||||
func buildReadyCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ready",
|
||||
Action: cliutil.ConfiguredAction(readyCommand),
|
||||
Usage: "Call /ready endpoint and return proper exit code",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] ready [subcommand options]",
|
||||
Description: "cloudflared tunnel ready will return proper exit code based on the /ready endpoint",
|
||||
Flags: []cli.Flag{},
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
}
|
||||
}
|
||||
|
||||
func readyCommand(c *cli.Context) error {
|
||||
metricsOpts := c.String("metrics")
|
||||
if !c.IsSet("metrics") {
|
||||
return fmt.Errorf("--metrics has to be provided")
|
||||
}
|
||||
|
||||
requestURL := fmt.Sprintf("http://%s/ready", metricsOpts)
|
||||
res, err := http.Get(requestURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("http://%s/ready endpoint returned status code %d\n%s", metricsOpts, res.StatusCode, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildInfoCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "info",
|
||||
|
||||
@@ -4,23 +4,23 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// Restrict key names to characters allowed in an HTTP header name.
|
||||
// Restrict key values to printable characters (what is recognised as data in an HTTP header value).
|
||||
var tagRegexp = regexp.MustCompile("^([a-zA-Z0-9!#$%&'*+\\-.^_`|~]+)=([[:print:]]+)$")
|
||||
|
||||
func NewTagFromCLI(compoundTag string) (tunnelpogs.Tag, bool) {
|
||||
func NewTagFromCLI(compoundTag string) (pogs.Tag, bool) {
|
||||
matches := tagRegexp.FindStringSubmatch(compoundTag)
|
||||
if len(matches) == 0 {
|
||||
return tunnelpogs.Tag{}, false
|
||||
return pogs.Tag{}, false
|
||||
}
|
||||
return tunnelpogs.Tag{Name: matches[1], Value: matches[2]}, true
|
||||
return pogs.Tag{Name: matches[1], Value: matches[2]}, true
|
||||
}
|
||||
|
||||
func NewTagSliceFromCLI(tags []string) ([]tunnelpogs.Tag, error) {
|
||||
var tagSlice []tunnelpogs.Tag
|
||||
func NewTagSliceFromCLI(tags []string) ([]pogs.Tag, error) {
|
||||
var tagSlice []pogs.Tag
|
||||
for _, compoundTag := range tags {
|
||||
if tag, ok := NewTagFromCLI(compoundTag); ok {
|
||||
tagSlice = append(tagSlice, tag)
|
||||
|
||||
@@ -3,7 +3,7 @@ package tunnel
|
||||
import (
|
||||
"testing"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
func TestSingleTag(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Output tunnelpogs.Tag
|
||||
Output pogs.Tag
|
||||
Fail bool
|
||||
}{
|
||||
{Input: "x=y", Output: tunnelpogs.Tag{Name: "x", Value: "y"}},
|
||||
{Input: "More-Complex=Tag Values", Output: tunnelpogs.Tag{Name: "More-Complex", Value: "Tag Values"}},
|
||||
{Input: "First=Equals=Wins", Output: tunnelpogs.Tag{Name: "First", Value: "Equals=Wins"}},
|
||||
{Input: "x=y", Output: pogs.Tag{Name: "x", Value: "y"}},
|
||||
{Input: "More-Complex=Tag Values", Output: pogs.Tag{Name: "More-Complex", Value: "Tag Values"}},
|
||||
{Input: "First=Equals=Wins", Output: pogs.Tag{Name: "First", Value: "Equals=Wins"}},
|
||||
{Input: "x=", Fail: true},
|
||||
{Input: "=y", Fail: true},
|
||||
{Input: "=", Fail: true},
|
||||
|
||||
@@ -21,6 +21,8 @@ var (
|
||||
Aliases: []string{"vn"},
|
||||
Usage: "The ID or name of the virtual network to which the route is associated to.",
|
||||
}
|
||||
|
||||
routeAddError = errors.New("You must supply exactly one argument, the ID or CIDR of the route you want to delete")
|
||||
)
|
||||
|
||||
func buildRouteIPSubcommand() *cli.Command {
|
||||
@@ -68,11 +70,9 @@ which virtual network's routing table you want to add the route to with:
|
||||
Name: "delete",
|
||||
Action: cliutil.ConfiguredAction(deleteRouteCommand),
|
||||
Usage: "Delete a row from your organization's private routing table",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]",
|
||||
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network
|
||||
will no longer be reachable by the WARP clients. Note that if you use virtual
|
||||
networks, then you have to tell which virtual network whose routing table you
|
||||
have a row deleted from.`,
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [Route ID or CIDR]",
|
||||
Description: `Deletes the row for the given route ID from your routing table. That portion of your network
|
||||
will no longer be reachable.`,
|
||||
Flags: []cli.Flag{vnetFlag},
|
||||
},
|
||||
{
|
||||
@@ -187,33 +187,36 @@ func deleteRouteCommand(c *cli.Context) error {
|
||||
}
|
||||
|
||||
if c.NArg() != 1 {
|
||||
return errors.New("You must supply exactly one argument, the network whose route you want to delete (in CIDR form e.g. 1.2.3.4/32)")
|
||||
return routeAddError
|
||||
}
|
||||
|
||||
_, network, err := net.ParseCIDR(c.Args().First())
|
||||
var routeId uuid.UUID
|
||||
routeId, err = uuid.Parse(c.Args().First())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid network CIDR")
|
||||
}
|
||||
if network == nil {
|
||||
return errors.New("Invalid network CIDR")
|
||||
}
|
||||
_, network, err := net.ParseCIDR(c.Args().First())
|
||||
if err != nil || network == nil {
|
||||
return routeAddError
|
||||
}
|
||||
|
||||
params := cfapi.DeleteRouteParams{
|
||||
Network: *network,
|
||||
}
|
||||
var vnetId *uuid.UUID
|
||||
if c.IsSet(vnetFlag.Name) {
|
||||
id, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vnetId = &id
|
||||
}
|
||||
|
||||
if c.IsSet(vnetFlag.Name) {
|
||||
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||
routeId, err = sc.getRouteId(*network, vnetId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params.VNetID = &vnetId
|
||||
}
|
||||
|
||||
if err := sc.deleteRoute(params); err != nil {
|
||||
if err := sc.deleteRoute(routeId); err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully deleted route for %s\n", network)
|
||||
fmt.Printf("Successfully deleted route with ID %s\n", routeId)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -269,7 +272,7 @@ func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
|
||||
defer writer.Flush()
|
||||
|
||||
// Print column headers with tabbed columns
|
||||
_, _ = fmt.Fprintln(writer, "NETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
||||
_, _ = fmt.Fprintln(writer, "ID\tNETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
||||
|
||||
// Loop through routes, create formatted string for each, and print using tabwriter
|
||||
for _, route := range routes {
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
@@ -31,7 +32,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
version string
|
||||
buildInfo *cliutil.BuildInfo
|
||||
BuiltForPackageManager = ""
|
||||
)
|
||||
|
||||
@@ -81,8 +82,8 @@ func (uo *UpdateOutcome) noUpdate() bool {
|
||||
return uo.Error == nil && uo.Updated == false
|
||||
}
|
||||
|
||||
func Init(v string) {
|
||||
version = v
|
||||
func Init(info *cliutil.BuildInfo) {
|
||||
buildInfo = info
|
||||
}
|
||||
|
||||
func CheckForUpdate(options updateOptions) (CheckResult, error) {
|
||||
@@ -100,11 +101,12 @@ func CheckForUpdate(options updateOptions) (CheckResult, error) {
|
||||
cfdPath = encodeWindowsPath(cfdPath)
|
||||
}
|
||||
|
||||
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
|
||||
s := NewWorkersService(buildInfo.CloudflaredVersion, url, cfdPath, Options{IsBeta: options.isBeta,
|
||||
IsForced: options.isForced, RequestedVersion: options.intendedVersion})
|
||||
|
||||
return s.Check()
|
||||
}
|
||||
|
||||
func encodeWindowsPath(path string) string {
|
||||
// We do this because Windows allows spaces in directories such as
|
||||
// Program Files but does not allow these directories to be spaced in batch files.
|
||||
@@ -196,10 +198,9 @@ func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome {
|
||||
|
||||
// AutoUpdater periodically checks for new version of cloudflared.
|
||||
type AutoUpdater struct {
|
||||
configurable *configurable
|
||||
listeners *gracenet.Net
|
||||
updateConfigChan chan *configurable
|
||||
log *zerolog.Logger
|
||||
configurable *configurable
|
||||
listeners *gracenet.Net
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
|
||||
@@ -210,10 +211,9 @@ type configurable struct {
|
||||
|
||||
func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
|
||||
return &AutoUpdater{
|
||||
configurable: createUpdateConfig(updateDisabled, freq, log),
|
||||
listeners: listeners,
|
||||
updateConfigChan: make(chan *configurable),
|
||||
log: log,
|
||||
configurable: createUpdateConfig(updateDisabled, freq, log),
|
||||
listeners: listeners,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,12 +232,20 @@ func createUpdateConfig(updateDisabled bool, freq time.Duration, log *zerolog.Lo
|
||||
}
|
||||
}
|
||||
|
||||
// Run will perodically check for cloudflared updates, download them, and then restart the current cloudflared process
|
||||
// to use the new version. It delays the first update check by the configured frequency as to not attempt a
|
||||
// download immediately and restart after starting (in the case that there is an upgrade available).
|
||||
func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
ticker := time.NewTicker(a.configurable.freq)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
|
||||
if updateOutcome.Updated {
|
||||
Init(updateOutcome.Version)
|
||||
buildInfo.CloudflaredVersion = updateOutcome.Version
|
||||
if IsSysV() {
|
||||
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
|
||||
a.log.Info().Msg("Restarting service managed by SysV...")
|
||||
@@ -254,25 +262,9 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
|
||||
} else if updateOutcome.UserMessage != "" {
|
||||
a.log.Warn().Msg(updateOutcome.UserMessage)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case newConfigurable := <-a.updateConfigChan:
|
||||
ticker.Stop()
|
||||
a.configurable = newConfigurable
|
||||
ticker = time.NewTicker(a.configurable.freq)
|
||||
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
|
||||
func (a *AutoUpdater) Update(updateDisabled bool, newFreq time.Duration) {
|
||||
a.updateConfigChan <- createUpdateConfig(updateDisabled, newFreq, a.log)
|
||||
}
|
||||
|
||||
func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool {
|
||||
if !supportAutoUpdate(log) {
|
||||
return false
|
||||
@@ -304,7 +296,7 @@ func wasInstalledFromPackageManager() bool {
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
func IsSysV() bool {
|
||||
|
||||
@@ -9,8 +9,14 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Init(cliutil.GetBuildInfo("TEST", "TEST"))
|
||||
}
|
||||
|
||||
func TestDisabledAutoUpdater(t *testing.T) {
|
||||
listeners := &gracenet.Net{}
|
||||
log := zerolog.Nop()
|
||||
|
||||
@@ -3,6 +3,7 @@ package updater
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
@@ -56,6 +57,9 @@ func (s *WorkersService) Check() (CheckResult, error) {
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, s.url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := req.URL.Query()
|
||||
q.Add(OSKeyName, runtime.GOOS)
|
||||
q.Add(ArchitectureKeyName, runtime.GOARCH)
|
||||
@@ -76,6 +80,10 @@ func (s *WorkersService) Check() (CheckResult, error) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("unable to check for update: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var v VersionResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package updater
|
||||
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -224,7 +222,7 @@ func TestUpdateService(t *testing.T) {
|
||||
require.Equal(t, v.Version(), mostRecentVersion)
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
dat, err := os.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), mostRecentVersion)
|
||||
@@ -243,7 +241,7 @@ func TestBetaUpdateService(t *testing.T) {
|
||||
require.Equal(t, v.Version(), mostRecentBetaVersion)
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
dat, err := os.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), mostRecentBetaVersion)
|
||||
@@ -289,7 +287,7 @@ func TestForcedUpdateService(t *testing.T) {
|
||||
require.Equal(t, v.Version(), mostRecentVersion)
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
dat, err := os.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, string(dat), mostRecentVersion)
|
||||
@@ -309,7 +307,7 @@ func TestUpdateSpecificVersionService(t *testing.T) {
|
||||
require.Equal(t, reqVersion, v.Version())
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
dat, err := os.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, reqVersion, string(dat))
|
||||
@@ -328,7 +326,7 @@ func TestCompressedUpdateService(t *testing.T) {
|
||||
require.Equal(t, "2020.09.02", v.Version())
|
||||
|
||||
require.NoError(t, v.Apply())
|
||||
dat, err := ioutil.ReadFile(testFilePath)
|
||||
dat, err := os.ReadFile(testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "2020.09.02", string(dat))
|
||||
|
||||
@@ -3,7 +3,6 @@ package updater
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -16,6 +15,10 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,9 +30,9 @@ const (
|
||||
// start the service
|
||||
// exit with code 0 if we've reached this point indicating success.
|
||||
windowsUpdateCommandTemplate = `sc stop cloudflared >nul 2>&1
|
||||
del "{{.OldPath}}"
|
||||
rename "{{.TargetPath}}" {{.OldName}}
|
||||
rename "{{.NewPath}}" {{.BinaryName}}
|
||||
del "{{.OldPath}}"
|
||||
sc start cloudflared >nul 2>&1
|
||||
exit /b 0`
|
||||
batchFileName = "cfd_update.bat"
|
||||
@@ -86,8 +89,25 @@ func (v *WorkersVersion) Apply() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// check that the file is what is expected
|
||||
if err := isValidChecksum(v.checksum, newFilePath); err != nil {
|
||||
downloadSum, err := cliutil.FileChecksum(newFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check that the file downloaded matches what is expected.
|
||||
if v.checksum != downloadSum {
|
||||
return errors.New("checksum validation failed")
|
||||
}
|
||||
|
||||
// Check if the currently running version has the same checksum
|
||||
if downloadSum == buildInfo.Checksum {
|
||||
// Currently running binary matches the downloaded binary so we have no reason to update. This is
|
||||
// typically unexpected, as such we emit a sentry event.
|
||||
localHub := sentry.CurrentHub().Clone()
|
||||
err := errors.New("checksum validation matches currently running process")
|
||||
localHub.CaptureException(err)
|
||||
// Make sure to cleanup the new downloaded file since we aren't upgrading versions.
|
||||
os.Remove(newFilePath)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -189,27 +209,6 @@ func isCompressedFile(urlstring string) bool {
|
||||
return strings.HasSuffix(u.Path, ".tgz")
|
||||
}
|
||||
|
||||
// checks if the checksum in the json response matches the checksum of the file download
|
||||
func isValidChecksum(checksum, filePath string) error {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
if checksum != hash {
|
||||
return errors.New("checksum validation failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeBatchFile writes a batch file out to disk
|
||||
// see the dicussion on why it has to be done this way
|
||||
func writeBatchFile(targetPath string, newPath string, oldPath string) error {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Requirements
|
||||
1. Python 3.7 or later with packages in the given `requirements.txt`
|
||||
- E.g. with conda:
|
||||
- `conda create -n component-tests python=3.7`
|
||||
- `conda activate component-tests`
|
||||
- `pip3 install -r requirements.txt`
|
||||
1. Python 3.10 or later with packages in the given `requirements.txt`
|
||||
- E.g. with venv:
|
||||
- `python3 -m venv ./.venv`
|
||||
- `source ./.venv/bin/activate`
|
||||
- `python3 -m pip install -r requirements.txt`
|
||||
|
||||
2. Create a config yaml file, for example:
|
||||
```
|
||||
|
||||
@@ -2,7 +2,9 @@ import json
|
||||
import subprocess
|
||||
from time import sleep
|
||||
|
||||
from constants import MANAGEMENT_HOST_NAME
|
||||
from setup import get_config_from_file
|
||||
from util import get_tunnel_connector_id
|
||||
|
||||
SINGLE_CASE_TIMEOUT = 600
|
||||
|
||||
@@ -28,6 +30,36 @@ class CloudflaredCli:
|
||||
listed = self._run_command(cmd_args, "list")
|
||||
return json.loads(listed.stdout)
|
||||
|
||||
def get_management_token(self, config, config_path):
|
||||
basecmd = [config.cloudflared_binary]
|
||||
if config_path is not None:
|
||||
basecmd += ["--config", str(config_path)]
|
||||
origincert = get_config_from_file()["origincert"]
|
||||
if origincert:
|
||||
basecmd += ["--origincert", origincert]
|
||||
|
||||
cmd_args = ["tail", "token", config.get_tunnel_id()]
|
||||
cmd = basecmd + cmd_args
|
||||
result = run_subprocess(cmd, "token", self.logger, check=True, capture_output=True, timeout=15)
|
||||
return json.loads(result.stdout.decode("utf-8").strip())["token"]
|
||||
|
||||
def get_management_url(self, path, config, config_path):
|
||||
access_jwt = self.get_management_token(config, config_path)
|
||||
connector_id = get_tunnel_connector_id()
|
||||
return f"https://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
|
||||
|
||||
def get_management_wsurl(self, path, config, config_path):
|
||||
access_jwt = self.get_management_token(config, config_path)
|
||||
connector_id = get_tunnel_connector_id()
|
||||
return f"wss://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
|
||||
|
||||
def get_connector_id(self, config):
|
||||
op = self.get_tunnel_info(config.get_tunnel_id())
|
||||
connectors = []
|
||||
for conn in op["conns"]:
|
||||
connectors.append(conn["id"])
|
||||
return connectors
|
||||
|
||||
def get_tunnel_info(self, tunnel_id):
|
||||
info = self._run_command(["info", "--output", "json", tunnel_id], "info")
|
||||
return json.loads(info.stdout)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
cloudflared_binary: "cloudflared"
|
||||
tunnel: "ae21a96c-24d1-4ce8-a6ba-962cba5976d3"
|
||||
credentials_file: "/Users/sudarsan/.cloudflared/ae21a96c-24d1-4ce8-a6ba-962cba5976d3.json"
|
||||
origincert: "/Users/sudarsan/.cloudflared/cert.pem"
|
||||
ingress:
|
||||
- hostname: named-tunnel-component-tests.example.com
|
||||
service: hello_world
|
||||
- service: http_status:404
|
||||
@@ -4,6 +4,7 @@ BACKOFF_SECS = 7
|
||||
MAX_LOG_LINES = 50
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
|
||||
|
||||
|
||||
def protocols():
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
cloudflare==2.8.15
|
||||
cloudflare==2.14.3
|
||||
flaky==3.7.0
|
||||
pytest==6.2.2
|
||||
pyyaml==5.4.1
|
||||
requests==2.25.1
|
||||
retrying==1.3.3
|
||||
pytest==7.3.1
|
||||
pytest-asyncio==0.21.0
|
||||
pyyaml==6.0.1
|
||||
requests==2.28.2
|
||||
retrying==1.3.4
|
||||
websockets==11.0.1
|
||||
|
||||
@@ -74,7 +74,7 @@ def delete_tunnel(config):
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def create_dns(config, hostname, type, content):
|
||||
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN"))
|
||||
cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN"))
|
||||
cf.zones.dns_records.post(
|
||||
config["zone_tag"],
|
||||
data={'name': hostname, 'type': type, 'content': content, 'proxied': True}
|
||||
@@ -89,7 +89,7 @@ def create_named_dns(config, random_uuid):
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def delete_dns(config, hostname):
|
||||
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN"))
|
||||
cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN"))
|
||||
zone_tag = config["zone_tag"]
|
||||
dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname})
|
||||
if len(dns_records) > 0:
|
||||
|
||||
@@ -36,17 +36,17 @@ class TestConfig:
|
||||
_ = start_cloudflared(tmp_path, config, validate_args)
|
||||
|
||||
self.match_rule(tmp_path, config,
|
||||
"http://example.com/index.html", 1)
|
||||
"http://example.com/index.html", 0)
|
||||
self.match_rule(tmp_path, config,
|
||||
"https://example.com/index.html", 1)
|
||||
"https://example.com/index.html", 0)
|
||||
self.match_rule(tmp_path, config,
|
||||
"https://api.example.com/login", 2)
|
||||
"https://api.example.com/login", 1)
|
||||
self.match_rule(tmp_path, config,
|
||||
"https://wss.example.com", 3)
|
||||
"https://wss.example.com", 2)
|
||||
self.match_rule(tmp_path, config,
|
||||
"https://ssh.example.com", 4)
|
||||
"https://ssh.example.com", 3)
|
||||
self.match_rule(tmp_path, config,
|
||||
"https://api.example.com", 5)
|
||||
"https://api.example.com", 4)
|
||||
|
||||
# This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. Note that rule number uses 1-based indexing
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ def assert_log_to_dir(config, log_dir):
|
||||
class TestLogging:
|
||||
def test_logging_to_terminal(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config()
|
||||
with start_cloudflared(tmp_path, config, new_process=True) as cloudflared:
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
assert_log_to_terminal(cloudflared)
|
||||
|
||||
@@ -85,7 +85,7 @@ class TestLogging:
|
||||
"logfile": str(log_file),
|
||||
}
|
||||
config = component_tests_config(extra_config)
|
||||
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_file))
|
||||
assert_log_in_file(log_file)
|
||||
assert_json_log(log_file)
|
||||
@@ -98,6 +98,6 @@ class TestLogging:
|
||||
"log-directory": str(log_dir),
|
||||
}
|
||||
config = component_tests_config(extra_config)
|
||||
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False):
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_dir))
|
||||
assert_log_to_dir(config, log_dir)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python
|
||||
import requests
|
||||
from conftest import CfdModes
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
from retrying import retry
|
||||
from cli import CloudflaredCli
|
||||
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
|
||||
import platform
|
||||
|
||||
"""
|
||||
Each test in TestManagement will:
|
||||
1. Acquire a management token from Cloudflare public API
|
||||
2. Make a request against the management service for the running tunnel
|
||||
"""
|
||||
class TestManagement:
|
||||
"""
|
||||
test_get_host_details does the following:
|
||||
1. It gets a management token from Tunnelstore using cloudflared tail token <tunnel_id>
|
||||
2. It gets the connector_id after starting a cloudflare tunnel
|
||||
3. It sends a request to the management host with the connector_id and management token
|
||||
4. Asserts that the response has a hostname and ip.
|
||||
"""
|
||||
def test_get_host_details(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
headers = {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
connector_id = cfd_cli.get_connector_id(config)[0]
|
||||
url = cfd_cli.get_management_url("host_details", config, config_path)
|
||||
resp = send_request(url, headers=headers)
|
||||
|
||||
# Assert response json.
|
||||
assert resp.status_code == 200, "Expected cloudflared to return 200 for host details"
|
||||
assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname"
|
||||
assert resp.json()["ip"] != "", "Expected cloudflared to return ip"
|
||||
assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id"
|
||||
|
||||
"""
|
||||
test_get_metrics will verify that the /metrics endpoint returns the prometheus metrics dump
|
||||
"""
|
||||
def test_get_metrics(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
assert resp.status_code == 200, "Expected cloudflared to return 200 for /metrics"
|
||||
assert "# HELP build_info Build and version information" in resp.text, "Expected /metrics to have with the build_info details"
|
||||
|
||||
"""
|
||||
test_get_pprof_heap will verify that the /debug/pprof/heap endpoint returns a pprof/heap dump response
|
||||
"""
|
||||
def test_get_pprof_heap(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path)
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
assert resp.status_code == 200, "Expected cloudflared to return 200 for /debug/pprof/heap"
|
||||
assert resp.headers["Content-Type"] == "application/octet-stream", "Expected /debug/pprof/heap to have return a binary response"
|
||||
|
||||
"""
|
||||
test_get_metrics_when_disabled will verify that diagnostic endpoints (such as /metrics) return 404 and are unmounted.
|
||||
"""
|
||||
def test_get_metrics_when_disabled(self, tmp_path, component_tests_config):
|
||||
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||
if platform.system() == "Windows":
|
||||
return
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||
resp = send_request(url)
|
||||
|
||||
# Assert response.
|
||||
assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics"
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def send_request(url, headers={}):
|
||||
with requests.Session() as s:
|
||||
return s.get(url, timeout=BACKOFF_SECS, headers=headers)
|
||||
@@ -12,6 +12,6 @@ class TestPostQuantum:
|
||||
def test_post_quantum(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(self._extra_config())
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--post-quantum"], new_process=True):
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--post-quantum"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=4)
|
||||
require_min_connections=1)
|
||||
|
||||
@@ -25,7 +25,7 @@ def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns)
|
||||
|
||||
if cfd_mode == CfdModes.NAMED:
|
||||
expect_tunnel = True
|
||||
pre_args = ["tunnel"]
|
||||
pre_args = ["tunnel", "--ha-connections", "1"]
|
||||
args = ["run"]
|
||||
elif cfd_mode == CfdModes.PROXY_DNS:
|
||||
expect_proxy_dns = True
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
from conftest import CfdModes
|
||||
from constants import METRICS_PORT
|
||||
import time
|
||||
from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_url, send_requests
|
||||
|
||||
class TestQuickTunnels:
|
||||
def test_quick_tunnel(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["--hello-world"], new_process=True):
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
time.sleep(10)
|
||||
url = get_quicktunnel_url()
|
||||
send_requests(url, 3, True)
|
||||
|
||||
def test_quick_tunnel_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready()
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
time.sleep(10)
|
||||
url = get_quicktunnel_url()
|
||||
send_requests(url+"/ready", 3, True)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connecte
|
||||
|
||||
@flaky(max_runs=3, min_passes=1)
|
||||
class TestReconnect:
|
||||
default_ha_conns = 4
|
||||
default_ha_conns = 1
|
||||
default_reconnect_secs = 15
|
||||
extra_config = {
|
||||
"stdin-control": True,
|
||||
@@ -29,7 +29,7 @@ class TestReconnect:
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_named_reconnect(self, tmp_path, component_tests_config, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(tmp_path, config, new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
# Repeat the test multiple times because some issues only occur after multiple reconnects
|
||||
self.assert_reconnect(config, cloudflared, 5)
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python
|
||||
import asyncio
|
||||
import json
|
||||
import pytest
|
||||
import requests
|
||||
import websockets
|
||||
from websockets.client import connect, WebSocketClientProtocol
|
||||
from conftest import CfdModes
|
||||
from constants import MAX_RETRIES, BACKOFF_SECS
|
||||
from retrying import retry
|
||||
from cli import CloudflaredCli
|
||||
from util import LOGGER, start_cloudflared, write_config, wait_tunnel_ready
|
||||
|
||||
class TestTail:
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_stop_streaming(self, tmp_path, component_tests_config):
|
||||
"""
|
||||
Validates that a websocket connection to management.argotunnel.com/logs can be opened
|
||||
with the access token and start and stop streaming on-demand.
|
||||
"""
|
||||
print("test_start_stop_streaming")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
async with connect(url, open_timeout=5, close_timeout=3) as websocket:
|
||||
await websocket.send('{"type": "start_streaming"}')
|
||||
await websocket.send('{"type": "stop_streaming"}')
|
||||
await websocket.send('{"type": "start_streaming"}')
|
||||
await websocket.send('{"type": "stop_streaming"}')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_logs(self, tmp_path, component_tests_config):
|
||||
"""
|
||||
Validates that a streaming logs connection will stream logs
|
||||
"""
|
||||
print("test_streaming_logs")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
# send start_streaming
|
||||
await websocket.send(json.dumps({
|
||||
"type": "start_streaming",
|
||||
"filters": {
|
||||
"events": ["http"]
|
||||
}
|
||||
}))
|
||||
# send some http requests to the tunnel to trigger some logs
|
||||
await generate_and_validate_http_events(websocket, config.get_url(), 10)
|
||||
# send stop_streaming
|
||||
await websocket.send('{"type": "stop_streaming"}')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_logs_filters(self, tmp_path, component_tests_config):
|
||||
"""
|
||||
Validates that a streaming logs connection will stream logs
|
||||
but not http when filters applied.
|
||||
"""
|
||||
print("test_streaming_logs_filters")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
# send start_streaming with tcp logs only
|
||||
await websocket.send(json.dumps({
|
||||
"type": "start_streaming",
|
||||
"filters": {
|
||||
"events": ["tcp"],
|
||||
"level": "debug"
|
||||
}
|
||||
}))
|
||||
# don't expect any http logs
|
||||
await generate_and_validate_no_log_event(websocket, config.get_url())
|
||||
# send stop_streaming
|
||||
await websocket.send('{"type": "stop_streaming"}')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_logs_sampling(self, tmp_path, component_tests_config):
|
||||
"""
|
||||
Validates that a streaming logs connection will stream logs with sampling.
|
||||
"""
|
||||
print("test_streaming_logs_sampling")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
# send start_streaming with info logs only
|
||||
await websocket.send(json.dumps({
|
||||
"type": "start_streaming",
|
||||
"filters": {
|
||||
"sampling": 0.5,
|
||||
"events": ["http"]
|
||||
}
|
||||
}))
|
||||
# don't expect any http logs
|
||||
count = await generate_and_validate_http_events(websocket, config.get_url(), 10)
|
||||
assert count < (10 * 2) # There are typically always two log lines for http requests (request and response)
|
||||
# send stop_streaming
|
||||
await websocket.send('{"type": "stop_streaming"}')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_logs_actor_override(self, tmp_path, component_tests_config):
|
||||
"""
|
||||
Validates that a streaming logs session can be overriden by the same actor
|
||||
"""
|
||||
print("test_streaming_logs_actor_override")
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
config_path = write_config(tmp_path, config.full_config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
|
||||
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||
url = cfd_cli.get_management_wsurl("logs", config, config_path)
|
||||
task = asyncio.ensure_future(start_streaming_to_be_remotely_closed(url))
|
||||
override_task = asyncio.ensure_future(start_streaming_override(url))
|
||||
await asyncio.wait([task, override_task])
|
||||
assert task.exception() == None, task.exception()
|
||||
assert override_task.exception() == None, override_task.exception()
|
||||
|
||||
async def start_streaming_to_be_remotely_closed(url):
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
try:
|
||||
await websocket.send(json.dumps({"type": "start_streaming"}))
|
||||
await asyncio.sleep(10)
|
||||
assert websocket.closed, "expected this request to be forcibly closed by the override"
|
||||
except websockets.ConnectionClosed:
|
||||
# we expect the request to be closed
|
||||
pass
|
||||
|
||||
async def start_streaming_override(url):
|
||||
# wait for the first connection to be established
|
||||
await asyncio.sleep(1)
|
||||
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
|
||||
await websocket.send(json.dumps({"type": "start_streaming"}))
|
||||
await asyncio.sleep(1)
|
||||
await websocket.send(json.dumps({"type": "stop_streaming"}))
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Every http request has two log lines sent
|
||||
async def generate_and_validate_http_events(websocket: WebSocketClientProtocol, url: str, count_send: int):
|
||||
for i in range(count_send):
|
||||
send_request(url)
|
||||
# There are typically always two log lines for http requests (request and response)
|
||||
count = 0
|
||||
while True:
|
||||
try:
|
||||
req_line = await asyncio.wait_for(websocket.recv(), 2)
|
||||
log_line = json.loads(req_line)
|
||||
assert log_line["type"] == "logs"
|
||||
assert log_line["logs"][0]["event"] == "http"
|
||||
count += 1
|
||||
except asyncio.TimeoutError:
|
||||
# ignore timeout from waiting for recv
|
||||
break
|
||||
return count
|
||||
|
||||
# Every http request has two log lines sent
|
||||
async def generate_and_validate_no_log_event(websocket: WebSocketClientProtocol, url: str):
|
||||
send_request(url)
|
||||
try:
|
||||
# wait for 5 seconds and make sure we hit the timeout and not recv any events
|
||||
req_line = await asyncio.wait_for(websocket.recv(), 5)
|
||||
assert req_line == None, "expected no logs for the specified filters"
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def send_request(url, headers={}):
|
||||
with requests.Session() as s:
|
||||
resp = s.get(url, timeout=BACKOFF_SECS, headers=headers)
|
||||
assert resp.status_code == 200, f"{url} returned {resp}"
|
||||
return resp.status_code == 200
|
||||
@@ -34,7 +34,7 @@ class TestTermination:
|
||||
def test_graceful_shutdown(self, tmp_path, component_tests_config, signal, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
|
||||
connected = threading.Condition()
|
||||
@@ -45,9 +45,10 @@ class TestTermination:
|
||||
with connected:
|
||||
connected.wait(self.timeout)
|
||||
# Send signal after the SSE connection is established
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
self.wait_eyeball_thread(
|
||||
in_flight_req, self.grace_period + self.timeout)
|
||||
with self.within_grace_period():
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
self.wait_eyeball_thread(
|
||||
in_flight_req, self.grace_period + self.timeout)
|
||||
|
||||
# test cloudflared terminates before grace period expires when all eyeball
|
||||
# connections are drained
|
||||
@@ -56,7 +57,7 @@ class TestTermination:
|
||||
def test_shutdown_once_no_connection(self, tmp_path, component_tests_config, signal, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
|
||||
connected = threading.Condition()
|
||||
@@ -66,7 +67,7 @@ class TestTermination:
|
||||
|
||||
with connected:
|
||||
connected.wait(self.timeout)
|
||||
with self.within_grace_period():
|
||||
with self.within_grace_period(has_connection=False):
|
||||
# Send signal after the SSE connection is established
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
self.wait_eyeball_thread(in_flight_req, self.grace_period)
|
||||
@@ -76,9 +77,9 @@ class TestTermination:
|
||||
def test_no_connection_shutdown(self, tmp_path, component_tests_config, signal, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
with self.within_grace_period():
|
||||
with self.within_grace_period(has_connection=False):
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
|
||||
def terminate_by_signal(self, cloudflared, sig):
|
||||
@@ -92,13 +93,21 @@ class TestTermination:
|
||||
|
||||
# Using this context asserts logic within the context is executed within grace period
|
||||
@contextmanager
|
||||
def within_grace_period(self):
|
||||
def within_grace_period(self, has_connection=True):
|
||||
try:
|
||||
start = time.time()
|
||||
yield
|
||||
finally:
|
||||
|
||||
# If the request takes longer than the grace period then we need to wait at most the grace period.
|
||||
# If the request fell within the grace period cloudflared can close earlier, but to ensure that it doesn't
|
||||
# close immediately we add a minimum boundary. If cloudflared shutdown in less than 1s it's likely that
|
||||
# it shutdown as soon as it received SIGINT. The only way cloudflared can close immediately is if it has no
|
||||
# in-flight requests
|
||||
minimum = 1 if has_connection else 0
|
||||
duration = time.time() - start
|
||||
assert duration < self.grace_period
|
||||
# Here we truncate to ensure that we don't fail on minute differences like 10.1 instead of 10
|
||||
assert minimum <= int(duration) <= self.grace_period
|
||||
|
||||
def stream_request(self, config, connected, early_terminate):
|
||||
expected_terminate_message = "502 Bad Gateway"
|
||||
|
||||
@@ -3,7 +3,9 @@ import requests
|
||||
from conftest import CfdModes
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
from retrying import retry
|
||||
from util import LOGGER, start_cloudflared, wait_tunnel_ready, send_requests
|
||||
from cli import CloudflaredCli
|
||||
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
|
||||
import platform
|
||||
|
||||
class TestTunnel:
|
||||
'''Test tunnels with no ingress rules from config.yaml but ingress rules from CLI only'''
|
||||
@@ -11,15 +13,15 @@ class TestTunnel:
|
||||
def test_tunnel_hello_world(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=4)
|
||||
require_min_connections=1)
|
||||
|
||||
def test_tunnel_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=4)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
send_requests(config.get_url()+"/ready", 3, True)
|
||||
|
||||
def test_tunnel_no_ingress(self, tmp_path, component_tests_config):
|
||||
@@ -29,8 +31,8 @@ class TestTunnel:
|
||||
'''
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=["run"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=4)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
resp = send_request(config.get_url()+"/")
|
||||
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
|
||||
resp = send_request(config.get_url()+"/test")
|
||||
@@ -38,6 +40,6 @@ class TestTunnel:
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def send_request(url):
|
||||
def send_request(url, headers={}):
|
||||
with requests.Session() as s:
|
||||
return s.get(url, timeout=BACKOFF_SECS)
|
||||
return s.get(url, timeout=BACKOFF_SECS, headers=headers)
|
||||
|
||||
+12
-4
@@ -4,17 +4,25 @@ import platform
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
from time import sleep
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
import json
|
||||
from retrying import retry
|
||||
|
||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
def configure_logger():
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
logger.addHandler(handler)
|
||||
return logger
|
||||
|
||||
LOGGER = configure_logger()
|
||||
|
||||
def select_platform(plat):
|
||||
return pytest.mark.skipif(
|
||||
@@ -48,7 +56,6 @@ def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel
|
||||
# By setting check=True, it will raise an exception if the process exits with non-zero exit code
|
||||
return subprocess.run(cmd, check=expect_success, capture_output=capture_output)
|
||||
|
||||
|
||||
def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
|
||||
cmd = []
|
||||
if root:
|
||||
@@ -106,13 +113,14 @@ def inner_wait_tunnel_ready(tunnel_url=None, require_min_connections=1):
|
||||
with requests.Session() as s:
|
||||
resp = send_request(s, metrics_url, True)
|
||||
|
||||
assert resp.json()["readyConnections"] >= require_min_connections, \
|
||||
ready_connections = resp.json()["readyConnections"]
|
||||
|
||||
assert ready_connections >= require_min_connections, \
|
||||
f"Ready endpoint returned {resp.json()} but we expect at least {require_min_connections} connections"
|
||||
|
||||
if tunnel_url is not None:
|
||||
send_request(s, tunnel_url, True)
|
||||
|
||||
|
||||
def _log_cloudflared_logs(cfd_logs):
|
||||
log_file = cfd_logs
|
||||
if os.path.isdir(cfd_logs):
|
||||
|
||||
@@ -205,6 +205,8 @@ type OriginRequestConfig struct {
|
||||
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
|
||||
// Hostname on the origin server certificate.
|
||||
OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"`
|
||||
// Auto configure the Hostname on the origin server certificate.
|
||||
MatchSNIToHost *bool `yaml:"matchSNItoHost" json:"matchSNItoHost,omitempty"`
|
||||
// Path to the CA for the certificate of your origin.
|
||||
// This option should be used only if your certificate is not signed by Cloudflare.
|
||||
CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
|
||||
@@ -257,7 +259,6 @@ type Configuration struct {
|
||||
}
|
||||
|
||||
type WarpRoutingConfig struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
|
||||
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ func TestConfigFileSettings(t *testing.T) {
|
||||
Service: "https://localhost:8001",
|
||||
}
|
||||
warpRouting = WarpRoutingConfig{
|
||||
Enabled: true,
|
||||
ConnectTimeout: &CustomDuration{Duration: 2 * time.Second},
|
||||
TCPKeepAlive: &CustomDuration{Duration: 10 * time.Second},
|
||||
}
|
||||
|
||||
@@ -36,14 +36,20 @@ var (
|
||||
flushableContentTypes = []string{sseContentType, grpcContentType}
|
||||
)
|
||||
|
||||
// TunnelConnection represents the connection to the edge.
|
||||
// The Serve method is provided to allow clients to handle any errors from the connection encountered during
|
||||
// processing of the connection. Cancelling of the context provided to Serve will close the connection.
|
||||
type TunnelConnection interface {
|
||||
Serve(ctx context.Context) error
|
||||
}
|
||||
|
||||
type Orchestrator interface {
|
||||
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
|
||||
GetConfigJSON() ([]byte, error)
|
||||
GetOriginProxy() (OriginProxy, error)
|
||||
WarpRoutingEnabled() (enabled bool)
|
||||
}
|
||||
|
||||
type NamedTunnelProperties struct {
|
||||
type TunnelProperties struct {
|
||||
Credentials Credentials
|
||||
Client pogs.ClientInfo
|
||||
QuickTunnelUrl string
|
||||
@@ -157,14 +163,16 @@ type ReadWriteAcker interface {
|
||||
type HTTPResponseReadWriteAcker struct {
|
||||
r io.Reader
|
||||
w ResponseWriter
|
||||
f http.Flusher
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
|
||||
func NewHTTPResponseReadWriterAcker(w ResponseWriter, req *http.Request) *HTTPResponseReadWriteAcker {
|
||||
func NewHTTPResponseReadWriterAcker(w ResponseWriter, flusher http.Flusher, req *http.Request) *HTTPResponseReadWriteAcker {
|
||||
return &HTTPResponseReadWriteAcker{
|
||||
r: req.Body,
|
||||
w: w,
|
||||
f: flusher,
|
||||
req: req,
|
||||
}
|
||||
}
|
||||
@@ -174,7 +182,11 @@ func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
|
||||
return h.w.Write(p)
|
||||
n, err := h.w.Write(p)
|
||||
if n > 0 {
|
||||
h.f.Flush()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
|
||||
|
||||
@@ -130,7 +130,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
wsCtx, cancel := context.WithCancel(r.Context())
|
||||
readPipe, writePipe := io.Pipe()
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
||||
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
|
||||
go func() {
|
||||
select {
|
||||
case <-wsCtx.Done():
|
||||
@@ -175,7 +176,7 @@ func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
wsCtx, cancel := context.WithCancel(r.Context())
|
||||
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
|
||||
|
||||
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
|
||||
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
|
||||
|
||||
+56
-33
@@ -6,25 +6,27 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// RPCClientFunc derives a named tunnel rpc client that can then be used to register and unregister connections.
|
||||
type RPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
// registerClient derives a named tunnel rpc client that can then be used to register and unregister connections.
|
||||
type registerClientFunc func(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient
|
||||
|
||||
type controlStream struct {
|
||||
observer *Observer
|
||||
|
||||
connectedFuse ConnectedFuse
|
||||
namedTunnelProperties *NamedTunnelProperties
|
||||
connIndex uint8
|
||||
edgeAddress net.IP
|
||||
protocol Protocol
|
||||
connectedFuse ConnectedFuse
|
||||
tunnelProperties *TunnelProperties
|
||||
connIndex uint8
|
||||
edgeAddress net.IP
|
||||
protocol Protocol
|
||||
|
||||
newRPCClientFunc RPCClientFunc
|
||||
registerClientFunc registerClientFunc
|
||||
registerTimeout time.Duration
|
||||
|
||||
gracefulShutdownC <-chan struct{}
|
||||
gracePeriod time.Duration
|
||||
@@ -47,27 +49,29 @@ type TunnelConfigJSONGetter interface {
|
||||
func NewControlStream(
|
||||
observer *Observer,
|
||||
connectedFuse ConnectedFuse,
|
||||
namedTunnelConfig *NamedTunnelProperties,
|
||||
tunnelProperties *TunnelProperties,
|
||||
connIndex uint8,
|
||||
edgeAddress net.IP,
|
||||
newRPCClientFunc RPCClientFunc,
|
||||
registerClientFunc registerClientFunc,
|
||||
registerTimeout time.Duration,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
protocol Protocol,
|
||||
) ControlStreamHandler {
|
||||
if newRPCClientFunc == nil {
|
||||
newRPCClientFunc = newRegistrationRPCClient
|
||||
if registerClientFunc == nil {
|
||||
registerClientFunc = tunnelrpc.NewRegistrationClient
|
||||
}
|
||||
return &controlStream{
|
||||
observer: observer,
|
||||
connectedFuse: connectedFuse,
|
||||
namedTunnelProperties: namedTunnelConfig,
|
||||
newRPCClientFunc: newRPCClientFunc,
|
||||
connIndex: connIndex,
|
||||
edgeAddress: edgeAddress,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
gracePeriod: gracePeriod,
|
||||
protocol: protocol,
|
||||
observer: observer,
|
||||
connectedFuse: connectedFuse,
|
||||
tunnelProperties: tunnelProperties,
|
||||
registerClientFunc: registerClientFunc,
|
||||
registerTimeout: registerTimeout,
|
||||
connIndex: connIndex,
|
||||
edgeAddress: edgeAddress,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
gracePeriod: gracePeriod,
|
||||
protocol: protocol,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,50 +81,69 @@ func (c *controlStream) ServeControlStream(
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
tunnelConfigGetter TunnelConfigJSONGetter,
|
||||
) error {
|
||||
rpcClient := c.newRPCClientFunc(ctx, rw, c.observer.log)
|
||||
registrationClient := c.registerClientFunc(ctx, rw, c.registerTimeout)
|
||||
|
||||
registrationDetails, err := rpcClient.RegisterConnection(ctx, c.namedTunnelProperties, connOptions, c.connIndex, c.edgeAddress, c.observer)
|
||||
registrationDetails, err := registrationClient.RegisterConnection(
|
||||
ctx,
|
||||
c.tunnelProperties.Credentials.Auth(),
|
||||
c.tunnelProperties.Credentials.TunnelID,
|
||||
connOptions,
|
||||
c.connIndex,
|
||||
c.edgeAddress)
|
||||
if err != nil {
|
||||
rpcClient.Close()
|
||||
return err
|
||||
defer registrationClient.Close()
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
c.observer.metrics.regFail.WithLabelValues("dup_edge_conn", "registerConnection").Inc()
|
||||
return errDuplicationConnection
|
||||
}
|
||||
c.observer.metrics.regFail.WithLabelValues("server_error", "registerConnection").Inc()
|
||||
return serverRegistrationErrorFromRPC(err)
|
||||
}
|
||||
c.observer.metrics.regSuccess.WithLabelValues("registerConnection").Inc()
|
||||
|
||||
c.observer.logConnected(registrationDetails.UUID, c.connIndex, registrationDetails.Location, c.edgeAddress, c.protocol)
|
||||
c.observer.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location)
|
||||
c.observer.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location, c.edgeAddress)
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
// if conn index is 0 and tunnel is not remotely managed, then send local ingress rules configuration
|
||||
if c.connIndex == 0 && !registrationDetails.TunnelIsRemotelyManaged {
|
||||
if tunnelConfig, err := tunnelConfigGetter.GetConfigJSON(); err == nil {
|
||||
if err := rpcClient.SendLocalConfiguration(ctx, tunnelConfig, c.observer); err != nil {
|
||||
if err := registrationClient.SendLocalConfiguration(ctx, tunnelConfig); err != nil {
|
||||
c.observer.metrics.localConfigMetrics.pushesErrors.Inc()
|
||||
c.observer.log.Err(err).Msg("unable to send local configuration")
|
||||
}
|
||||
c.observer.metrics.localConfigMetrics.pushes.Inc()
|
||||
} else {
|
||||
c.observer.log.Err(err).Msg("failed to obtain current configuration")
|
||||
}
|
||||
}
|
||||
|
||||
c.waitForUnregister(ctx, rpcClient)
|
||||
return nil
|
||||
return c.waitForUnregister(ctx, registrationClient)
|
||||
}
|
||||
|
||||
func (c *controlStream) waitForUnregister(ctx context.Context, rpcClient NamedTunnelRPCClient) {
|
||||
func (c *controlStream) waitForUnregister(ctx context.Context, registrationClient tunnelrpc.RegistrationClient) error {
|
||||
// wait for connection termination or start of graceful shutdown
|
||||
defer rpcClient.Close()
|
||||
defer registrationClient.Close()
|
||||
var shutdownError error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
shutdownError = ctx.Err()
|
||||
break
|
||||
case <-c.gracefulShutdownC:
|
||||
c.stoppedGracefully = true
|
||||
}
|
||||
|
||||
c.observer.sendUnregisteringEvent(c.connIndex)
|
||||
rpcClient.GracefulShutdown(ctx, c.gracePeriod)
|
||||
err := registrationClient.GracefulShutdown(ctx, c.gracePeriod)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error shutting down control stream")
|
||||
}
|
||||
c.observer.log.Info().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Uint8(LogFieldConnIndex, c.connIndex).
|
||||
IPAddr(LogFieldIPAddress, c.edgeAddress).
|
||||
Msg("Unregistered tunnel connection")
|
||||
return shutdownError
|
||||
}
|
||||
|
||||
func (c *controlStream) IsStopped() bool {
|
||||
|
||||
@@ -2,7 +2,6 @@ package connection
|
||||
|
||||
import (
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
@@ -71,8 +70,6 @@ func isHandshakeErrRecoverable(err error, connIndex uint8, observer *Observer) b
|
||||
switch err.(type) {
|
||||
case edgediscovery.DialError:
|
||||
log.Error().Msg("Connection unable to dial edge")
|
||||
case h2mux.MuxerHandshakeError:
|
||||
log.Error().Msg("Connection handshake with edge server failed")
|
||||
default:
|
||||
log.Error().Msg("Connection failed")
|
||||
return false
|
||||
|
||||
+8
-5
@@ -1,12 +1,15 @@
|
||||
package connection
|
||||
|
||||
import "net"
|
||||
|
||||
// Event is something that happened to a connection, e.g. disconnection or registration.
|
||||
type Event struct {
|
||||
Index uint8
|
||||
EventType Status
|
||||
Location string
|
||||
Protocol Protocol
|
||||
URL string
|
||||
Index uint8
|
||||
EventType Status
|
||||
Location string
|
||||
Protocol Protocol
|
||||
URL string
|
||||
EdgeAddress net.IP
|
||||
}
|
||||
|
||||
// Status is the status of a connection.
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
const (
|
||||
muxerTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type MuxerConfig struct {
|
||||
HeartbeatInterval time.Duration
|
||||
MaxHeartbeats uint64
|
||||
CompressionSetting h2mux.CompressionSetting
|
||||
MetricsUpdateFreq time.Duration
|
||||
}
|
||||
|
||||
func (mc *MuxerConfig) H2MuxerConfig(h h2mux.MuxedStreamHandler, log *zerolog.Logger) *h2mux.MuxerConfig {
|
||||
return &h2mux.MuxerConfig{
|
||||
Timeout: muxerTimeout,
|
||||
Handler: h,
|
||||
IsClient: true,
|
||||
HeartbeatInterval: mc.HeartbeatInterval,
|
||||
MaxHeartbeats: mc.MaxHeartbeats,
|
||||
Log: log,
|
||||
CompressionQuality: mc.CompressionSetting,
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
|
||||
// to an HTTP/1 Request object destined for the local origin web service.
|
||||
// This operation includes conversion of the pseudo-headers into their closest
|
||||
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
||||
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
name := strings.ToLower(header.Name)
|
||||
if !IsH2muxControlRequestHeader(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
// noop - use the preexisting scheme from h1.URL
|
||||
case ":authority":
|
||||
// Otherwise the host header will be based on the origin URL
|
||||
h1.Host = header.Value
|
||||
case ":path":
|
||||
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
|
||||
// However, this HTTP/1 Request object belongs to the Go standard library,
|
||||
// whose URL package makes some opinionated decisions about the encoding of
|
||||
// URL characters: see the docs of https://godoc.org/net/url#URL,
|
||||
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
|
||||
// which is always used when computing url.URL.String(), whether we'd like it or not.
|
||||
//
|
||||
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
|
||||
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
|
||||
// is treated differently when HTTP_PROXY is set!
|
||||
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
|
||||
//
|
||||
// This means we are subject to the behavior of net/url's function `shouldEscape`
|
||||
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
|
||||
|
||||
if header.Value == "*" {
|
||||
h1.URL.Path = "*"
|
||||
continue
|
||||
}
|
||||
// Due to the behavior of validation.ValidateUrl, h1.URL may
|
||||
// already have a partial value, with or without a trailing slash.
|
||||
base := h1.URL.String()
|
||||
base = strings.TrimRight(base, "/")
|
||||
// But we know :path begins with '/', because we handled '*' above - see RFC7540
|
||||
requestURL, err := url.Parse(base + header.Value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
|
||||
}
|
||||
h1.URL = requestURL
|
||||
case "content-length":
|
||||
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unparseable content length")
|
||||
}
|
||||
h1.ContentLength = contentLength
|
||||
case RequestUserHeaders:
|
||||
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
|
||||
// Find and parse user headers serialized into a single one
|
||||
userHeaders, err := DeserializeHeaders(header.Value)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to parse user headers")
|
||||
}
|
||||
for _, userHeader := range userHeaders {
|
||||
h1.Header.Add(userHeader.Name, userHeader.Value)
|
||||
}
|
||||
default:
|
||||
// All other control headers shall just be proxied transparently
|
||||
h1.Header.Add(header.Name, header.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
|
||||
h2 = []h2mux.Header{
|
||||
{Name: ":status", Value: strconv.Itoa(status)},
|
||||
}
|
||||
userHeaders := make(http.Header, len(h1))
|
||||
for header, values := range h1 {
|
||||
h2name := strings.ToLower(header)
|
||||
if h2name == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
|
||||
} else if !IsH2muxControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
|
||||
// User headers, on the other hand, must all be serialized so that
|
||||
// HTTP/2 header validation won't be applied to HTTP/1 header values
|
||||
userHeaders[header] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
|
||||
return h2
|
||||
}
|
||||
|
||||
// IsH2muxControlRequestHeader is called in the direction of eyeball -> origin.
|
||||
func IsH2muxControlRequestHeader(headerName string) bool {
|
||||
return headerName == "content-length" ||
|
||||
headerName == "connection" || headerName == "upgrade" || // Websocket request headers
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-")
|
||||
}
|
||||
|
||||
// IsH2muxControlResponseHeader is called in the direction of eyeball <- origin.
|
||||
func IsH2muxControlResponseHeader(headerName string) bool {
|
||||
return headerName == "content-length" ||
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-int-") ||
|
||||
strings.HasPrefix(headerName, "cf-cloudflared-")
|
||||
}
|
||||
@@ -1,642 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
type ByName []h2mux.Header
|
||||
|
||||
func (a ByName) Len() int { return len(a) }
|
||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByName) Less(i, j int) bool {
|
||||
if a[i].Name == a[j].Name {
|
||||
return a[i].Value < a[j].Value
|
||||
}
|
||||
|
||||
return a[i].Name < a[j].Name
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockHeaders := http.Header{
|
||||
"Mock header 1": {"Mock value 1"},
|
||||
"Mock header 2": {"Mock value 2"},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
|
||||
return []h2mux.Header{{
|
||||
Name: headersField,
|
||||
Value: SerializeHeaders(headers),
|
||||
}}
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
emptyHeaders := make(http.Header)
|
||||
headersConversionErr := H2RequestHeadersToH1Request(
|
||||
[]h2mux.Header{{
|
||||
Name: RequestUserHeaders,
|
||||
Value: SerializeHeaders(emptyHeaders),
|
||||
}},
|
||||
request,
|
||||
)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header))
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: "//bad_path/"},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: "/?query=mock%20value"},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: "/mock%20path"},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
|
||||
assert.Equal(t, http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
}, request.Header)
|
||||
|
||||
assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
|
||||
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
|
||||
type testCase struct {
|
||||
path string
|
||||
want string
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
path: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
want: "/",
|
||||
},
|
||||
{
|
||||
path: "//",
|
||||
want: "//",
|
||||
},
|
||||
{
|
||||
path: "/test",
|
||||
want: "/test",
|
||||
},
|
||||
{
|
||||
path: "//test",
|
||||
want: "//test",
|
||||
},
|
||||
{
|
||||
// https://github.com/cloudflare/cloudflared/issues/81
|
||||
path: "//test/",
|
||||
want: "//test/",
|
||||
},
|
||||
{
|
||||
path: "/%2Ftest",
|
||||
want: "/%2Ftest",
|
||||
},
|
||||
{
|
||||
path: "//%20test",
|
||||
want: "//%20test",
|
||||
},
|
||||
{
|
||||
// https://github.com/cloudflare/cloudflared/issues/124
|
||||
path: "/test?get=somthing%20a",
|
||||
want: "/test?get=somthing%20a",
|
||||
},
|
||||
{
|
||||
path: "/%20",
|
||||
want: "/%20",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode ' '
|
||||
path: "/ ",
|
||||
want: "/%20",
|
||||
},
|
||||
{
|
||||
path: "/ a ",
|
||||
want: "/%20a%20",
|
||||
},
|
||||
{
|
||||
path: "/a%20b",
|
||||
want: "/a%20b",
|
||||
},
|
||||
{
|
||||
path: "/foo/bar;param?query#frag",
|
||||
want: "/foo/bar;param?query#frag",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode non-ASCII chars
|
||||
path: "/a␠b",
|
||||
want: "/a%E2%90%A0b",
|
||||
},
|
||||
{
|
||||
path: "/a-umlaut-ä",
|
||||
want: "/a-umlaut-%C3%A4",
|
||||
},
|
||||
{
|
||||
path: "/a-umlaut-%C3%A4",
|
||||
want: "/a-umlaut-%C3%A4",
|
||||
},
|
||||
{
|
||||
path: "/a-umlaut-%c3%a4",
|
||||
want: "/a-umlaut-%c3%a4",
|
||||
},
|
||||
{
|
||||
// here the second '#' is treated as part of the fragment
|
||||
path: "/a#b#c",
|
||||
want: "/a#b%23c",
|
||||
},
|
||||
{
|
||||
path: "/a#b␠c",
|
||||
want: "/a#b%E2%90%A0c",
|
||||
},
|
||||
{
|
||||
path: "/a#b%20c",
|
||||
want: "/a#b%20c",
|
||||
},
|
||||
{
|
||||
path: "/a#b c",
|
||||
want: "/a#b%20c",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode '\'
|
||||
path: "/\\",
|
||||
want: "/%5C",
|
||||
},
|
||||
{
|
||||
path: "/a\\",
|
||||
want: "/a%5C",
|
||||
},
|
||||
{
|
||||
path: "/a,b.c.",
|
||||
want: "/a,b.c.",
|
||||
},
|
||||
{
|
||||
path: "/.",
|
||||
want: "/.",
|
||||
},
|
||||
{
|
||||
// stdlib's EscapedPath() will always percent-encode '`'
|
||||
path: "/a`",
|
||||
want: "/a%60",
|
||||
},
|
||||
{
|
||||
path: "/a[0]",
|
||||
want: "/a[0]",
|
||||
},
|
||||
{
|
||||
path: "/?a[0]=5 &b[]=",
|
||||
want: "/?a[0]=5 &b[]=",
|
||||
},
|
||||
{
|
||||
path: "/?a=%22b%20%22",
|
||||
want: "/?a=%22b%20%22",
|
||||
},
|
||||
}
|
||||
|
||||
for index, testCase := range testCases {
|
||||
requestURL := "https://example.com"
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockRequestHeaders := []h2mux.Header{
|
||||
{Name: ":path", Value: testCase.path},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
|
||||
assert.NoError(t, headersConversionErr)
|
||||
|
||||
assert.Equal(t,
|
||||
http.Header{
|
||||
"Mock header": []string{"Mock value"},
|
||||
},
|
||||
request.Header)
|
||||
|
||||
assert.Equal(t,
|
||||
"https://example.com"+testCase.want,
|
||||
request.URL.String(),
|
||||
"Failed URL index: %v %#v", index, testCase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
|
||||
config := &quick.Config{
|
||||
Values: func(args []reflect.Value, rand *rand.Rand) {
|
||||
args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
|
||||
},
|
||||
}
|
||||
|
||||
type testOrigin struct {
|
||||
url string
|
||||
|
||||
expectedScheme string
|
||||
expectedBasePath string
|
||||
}
|
||||
testOrigins := []testOrigin{
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080",
|
||||
},
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080/",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080",
|
||||
},
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080/api",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080/api",
|
||||
},
|
||||
{
|
||||
url: "http://origin.hostname.example.com:8080/api/",
|
||||
expectedScheme: "http",
|
||||
expectedBasePath: "http://origin.hostname.example.com:8080/api",
|
||||
},
|
||||
{
|
||||
url: "https://origin.hostname.example.com:8080/api",
|
||||
expectedScheme: "https",
|
||||
expectedBasePath: "https://origin.hostname.example.com:8080/api",
|
||||
},
|
||||
}
|
||||
|
||||
// use multiple schemes to demonstrate that the URL is based on the
|
||||
// origin's scheme, not the :scheme header
|
||||
for _, testScheme := range []string{"http", "https"} {
|
||||
for _, testOrigin := range testOrigins {
|
||||
assertion := func(testPath string) bool {
|
||||
const expectedMethod = "POST"
|
||||
const expectedHostname = "request.hostname.example.com"
|
||||
|
||||
h2 := []h2mux.Header{
|
||||
{Name: ":method", Value: expectedMethod},
|
||||
{Name: ":scheme", Value: testScheme},
|
||||
{Name: ":authority", Value: expectedHostname},
|
||||
{Name: ":path", Value: testPath},
|
||||
{Name: RequestUserHeaders, Value: ""},
|
||||
}
|
||||
h1, err := http.NewRequest("GET", testOrigin.url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = H2RequestHeadersToH1Request(h2, h1)
|
||||
return assert.NoError(t, err) &&
|
||||
assert.Equal(t, expectedMethod, h1.Method) &&
|
||||
assert.Equal(t, expectedHostname, h1.Host) &&
|
||||
assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
|
||||
assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
|
||||
}
|
||||
err := quick.Check(assertion, config)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func randomASCIIPrintableChar(rand *rand.Rand) int {
|
||||
// smallest printable ASCII char is 32, largest is 126
|
||||
const startPrintable = 32
|
||||
const endPrintable = 127
|
||||
return startPrintable + rand.Intn(endPrintable-startPrintable)
|
||||
}
|
||||
|
||||
// randomASCIIText generates an ASCII string, some of whose characters may be
|
||||
// percent-encoded. Its "logical length" (ignoring percent-encoding) is
|
||||
// between 1 and `maxLength`.
|
||||
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
|
||||
length := minLength + rand.Intn(maxLength)
|
||||
var result strings.Builder
|
||||
for i := 0; i < length; i++ {
|
||||
c := randomASCIIPrintableChar(rand)
|
||||
|
||||
// 1/4 chance of using percent encoding when not necessary
|
||||
if c == '%' || rand.Intn(4) == 0 {
|
||||
result.WriteString(fmt.Sprintf("%%%02X", c))
|
||||
} else {
|
||||
result.WriteByte(byte(c))
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Calls `randomASCIIText` and ensures the result is a valid URL path,
|
||||
// i.e. one that can pass unchanged through url.URL.String()
|
||||
func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
|
||||
text := randomASCIIText(rand, minLength, maxLength)
|
||||
re, err := regexp.Compile("[^/;,]*")
|
||||
require.NoError(t, err)
|
||||
return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
|
||||
}
|
||||
|
||||
// Calls `randomASCIIText` and ensures the result is a valid URL query,
|
||||
// i.e. one that can pass unchanged through url.URL.String()
|
||||
func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
|
||||
text := randomASCIIText(rand, minLength, maxLength)
|
||||
return "?" + strings.ReplaceAll(text, "#", "%23")
|
||||
}
|
||||
|
||||
// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
|
||||
// i.e. one that can pass unchanged through url.URL.String()
|
||||
func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
|
||||
text := randomASCIIText(rand, minLength, maxLength)
|
||||
u, err := url.Parse("#" + text)
|
||||
require.NoError(t, err)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// Assemble a random :path pseudoheader that is legal by Go stdlib standards
|
||||
// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
|
||||
func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
|
||||
result := randomHTTP1Path(t, rand, 1, 64)
|
||||
if rand.Intn(2) == 1 {
|
||||
result += randomHTTP1Query(rand, 1, 32)
|
||||
}
|
||||
if rand.Intn(2) == 1 {
|
||||
result += randomHTTP1Fragment(t, rand, 1, 16)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) {
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value})
|
||||
}
|
||||
}
|
||||
|
||||
return h2muxHeaders
|
||||
}
|
||||
|
||||
func TestParseRequestHeaders(t *testing.T) {
|
||||
mockUserHeadersToSerialize := http.Header{
|
||||
"Mock-Header-One": {"1", "1.5"},
|
||||
"Mock-Header-Two": {"2"},
|
||||
"Mock-Header-Three": {"3"},
|
||||
}
|
||||
|
||||
mockHeaders := []h2mux.Header{
|
||||
{Name: "One", Value: "1"}, // will be dropped
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
{Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)},
|
||||
}
|
||||
|
||||
expectedHeaders := []h2mux.Header{
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
{Name: "Mock-Header-One", Value: "1"},
|
||||
{Name: "Mock-Header-One", Value: "1.5"},
|
||||
{Name: "Mock-Header-Two", Value: "2"},
|
||||
{Name: "Mock-Header-Three", Value: "3"},
|
||||
}
|
||||
h1 := &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
err := H2RequestHeadersToH1Request(mockHeaders, h1)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
|
||||
}
|
||||
|
||||
func TestIsH2muxControlRequestHeader(t *testing.T) {
|
||||
controlRequestHeaders := []string{
|
||||
// Anything that begins with cf-
|
||||
"cf-sample-header",
|
||||
|
||||
// Any http2 pseudoheader
|
||||
":sample-pseudo-header",
|
||||
|
||||
// content-length is a special case, it has to be there
|
||||
// for some requests to work (per the HTTP2 spec)
|
||||
"content-length",
|
||||
|
||||
// Websocket request headers
|
||||
"connection",
|
||||
"upgrade",
|
||||
}
|
||||
|
||||
for _, header := range controlRequestHeaders {
|
||||
assert.True(t, IsH2muxControlRequestHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsH2muxControlResponseHeader(t *testing.T) {
|
||||
controlResponseHeaders := []string{
|
||||
// Anything that begins with cf-int- or cf-cloudflared-
|
||||
"cf-int-sample-header",
|
||||
"cf-cloudflared-sample-header",
|
||||
|
||||
// Any http2 pseudoheader
|
||||
":sample-pseudo-header",
|
||||
|
||||
// content-length is a special case, it has to be there
|
||||
// for some requests to work (per the HTTP2 spec)
|
||||
"content-length",
|
||||
}
|
||||
|
||||
for _, header := range controlResponseHeaders {
|
||||
assert.True(t, IsH2muxControlResponseHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotH2muxControlRequestHeader(t *testing.T) {
|
||||
notControlRequestHeaders := []string{
|
||||
"mock-header",
|
||||
"another-sample-header",
|
||||
}
|
||||
|
||||
for _, header := range notControlRequestHeaders {
|
||||
assert.False(t, IsH2muxControlRequestHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotH2muxControlResponseHeader(t *testing.T) {
|
||||
notControlResponseHeaders := []string{
|
||||
"mock-header",
|
||||
"another-sample-header",
|
||||
"upgrade",
|
||||
"connection",
|
||||
"cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared-
|
||||
}
|
||||
|
||||
for _, header := range notControlResponseHeaders {
|
||||
assert.False(t, IsH2muxControlResponseHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
mockHeaders := http.Header{
|
||||
"User-header-one": {""},
|
||||
"User-header-two": {"1", "2"},
|
||||
"cf-header": {"cf-value"},
|
||||
"cf-int-header": {"cf-int-value"},
|
||||
"cf-cloudflared-header": {"cf-cloudflared-value"},
|
||||
"Content-Length": {"123"},
|
||||
}
|
||||
mockResponse := http.Response{
|
||||
StatusCode: 200,
|
||||
Header: mockHeaders,
|
||||
}
|
||||
|
||||
headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
|
||||
|
||||
serializedHeadersIndex := -1
|
||||
for i, header := range headers {
|
||||
if header.Name == ResponseUserHeaders {
|
||||
serializedHeadersIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotEqual(t, -1, serializedHeadersIndex)
|
||||
actualControlHeaders := append(
|
||||
headers[:serializedHeadersIndex],
|
||||
headers[serializedHeadersIndex+1:]...,
|
||||
)
|
||||
expectedControlHeaders := []h2mux.Header{
|
||||
{Name: ":status", Value: "200"},
|
||||
{Name: "content-length", Value: "123"},
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
|
||||
|
||||
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
|
||||
expectedUserHeaders := []h2mux.Header{
|
||||
{Name: "User-header-one", Value: ""},
|
||||
{Name: "User-header-two", Value: "1"},
|
||||
{Name: "User-header-two", Value: "2"},
|
||||
{Name: "cf-header", Value: "cf-value"},
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
|
||||
}
|
||||
|
||||
// The purpose of this test is to check that our code and the http.Header
|
||||
// implementation don't throw validation errors about header size
|
||||
func TestHeaderSize(t *testing.T) {
|
||||
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
|
||||
largeHeaders := http.Header{
|
||||
"User-header": {largeValue},
|
||||
}
|
||||
mockResponse := http.Response{
|
||||
StatusCode: 200,
|
||||
Header: largeHeaders,
|
||||
}
|
||||
|
||||
serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
|
||||
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
for _, header := range serializedHeaders {
|
||||
request.Header.Set(header.Name, header.Value)
|
||||
}
|
||||
|
||||
for _, header := range serializedHeaders {
|
||||
if header.Name != ResponseUserHeaders {
|
||||
continue
|
||||
}
|
||||
|
||||
deserializedHeaders, err := DeserializeHeaders(header.Value)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func randSeq(n int) string {
|
||||
randomizer := rand.New(rand.NewSource(17))
|
||||
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[randomizer.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
|
||||
ser := "eC1mb3J3YXJkZWQtcHJvdG8:aHR0cHM;dXBncmFkZS1pbnNlY3VyZS1yZXF1ZXN0cw:MQ;YWNjZXB0LWxhbmd1YWdl:ZW4tVVMsZW47cT0wLjkscnU7cT0wLjg;YWNjZXB0LWVuY29kaW5n:Z3ppcA;eC1mb3J3YXJkZWQtZm9y:MTczLjI0NS42MC42;dXNlci1hZ2VudA:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTRfNikgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzg0LjAuNDE0Ny44OSBTYWZhcmkvNTM3LjM2;c2VjLWZldGNoLW1vZGU:bmF2aWdhdGU;Y2RuLWxvb3A:Y2xvdWRmbGFyZQ;c2VjLWZldGNoLWRlc3Q:ZG9jdW1lbnQ;c2VjLWZldGNoLXVzZXI:PzE;c2VjLWZldGNoLXNpdGU:bm9uZQ;Y29va2ll:X19jZmR1aWQ9ZGNkOWZjOGNjNWMxMzE0NTMyYTFkMjhlZDEyOWRhOTYwMTU2OTk1MTYzNDsgX19jZl9ibT1mYzY2MzMzYzAzZmM0MWFiZTZmOWEyYzI2ZDUwOTA0YzIxYzZhMTQ2LTE1OTU2MjIzNDEtMTgwMC1BZTVzS2pIU2NiWGVFM05mMUhrTlNQMG1tMHBLc2pQWkloVnM1Z2g1SkNHQkFhS1UxVDB2b003alBGN3FjMHVSR2NjZGcrWHdhL1EzbTJhQzdDVU4xZ2M9;YWNjZXB0:dGV4dC9odG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCxhcHBsaWNhdGlvbi94bWw7cT0wLjksaW1hZ2Uvd2VicCxpbWFnZS9hcG5nLCovKjtxPTAuOCxhcHBsaWNhdGlvbi9zaWduZWQtZXhjaGFuZ2U7dj1iMztxPTAuOQ"
|
||||
h2, _ := DeserializeHeaders(ser)
|
||||
h1 := make(http.Header)
|
||||
for _, header := range h2 {
|
||||
h1.Add(header.Name, header.Value)
|
||||
}
|
||||
h1.Add("Content-Length", "200")
|
||||
h1.Add("Cf-Something", "Else")
|
||||
h1.Add("Upgrade", "websocket")
|
||||
|
||||
h1resp := &http.Response{
|
||||
StatusCode: 200,
|
||||
Header: h1,
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
|
||||
}
|
||||
}
|
||||
+12
-7
@@ -7,17 +7,15 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
var (
|
||||
// h2mux-style special headers
|
||||
// internal special headers
|
||||
RequestUserHeaders = "cf-cloudflared-request-headers"
|
||||
ResponseUserHeaders = "cf-cloudflared-response-headers"
|
||||
ResponseMetaHeader = "cf-cloudflared-response-meta"
|
||||
|
||||
// h2mux-style special headers
|
||||
// internal special headers
|
||||
CanonicalResponseUserHeaders = http.CanonicalHeaderKey(ResponseUserHeaders)
|
||||
CanonicalResponseMetaHeader = http.CanonicalHeaderKey(ResponseMetaHeader)
|
||||
)
|
||||
@@ -28,6 +26,13 @@ var (
|
||||
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
|
||||
)
|
||||
|
||||
// HTTPHeader is a custom header struct that expects only ever one value for the header.
|
||||
// This structure is used to serialize the headers and attach them to the HTTP2 request when proxying.
|
||||
type HTTPHeader struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type responseMetaHeader struct {
|
||||
Source string `json:"src"`
|
||||
}
|
||||
@@ -104,10 +109,10 @@ func SerializeHeaders(h1Headers http.Header) string {
|
||||
}
|
||||
|
||||
// Deserialize headers serialized by `SerializeHeader`
|
||||
func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) {
|
||||
func DeserializeHeaders(serializedHeaders string) ([]HTTPHeader, error) {
|
||||
const unableToDeserializeErr = "Unable to deserialize headers"
|
||||
|
||||
var deserialized []h2mux.Header
|
||||
var deserialized []HTTPHeader
|
||||
for _, serializedPair := range strings.Split(serializedHeaders, ";") {
|
||||
if len(serializedPair) == 0 {
|
||||
continue
|
||||
@@ -130,7 +135,7 @@ func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) {
|
||||
return nil, errors.Wrap(err, unableToDeserializeErr)
|
||||
}
|
||||
|
||||
deserialized = append(deserialized, h2mux.Header{
|
||||
deserialized = append(deserialized, HTTPHeader{
|
||||
Name: string(deserializedName),
|
||||
Value: string(deserializedValue),
|
||||
})
|
||||
|
||||
@@ -46,18 +46,40 @@ func TestSerializeHeaders(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 13, len(deserializedHeaders))
|
||||
h2muxExpectedHeaders := stdlibHeaderToH2muxHeader(mockHeaders)
|
||||
expectedHeaders := headerToReqHeader(mockHeaders)
|
||||
|
||||
sort.Sort(ByName(deserializedHeaders))
|
||||
sort.Sort(ByName(h2muxExpectedHeaders))
|
||||
sort.Sort(ByName(expectedHeaders))
|
||||
|
||||
assert.True(
|
||||
t,
|
||||
reflect.DeepEqual(h2muxExpectedHeaders, deserializedHeaders),
|
||||
fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, h2muxExpectedHeaders),
|
||||
reflect.DeepEqual(expectedHeaders, deserializedHeaders),
|
||||
fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, expectedHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type ByName []HTTPHeader
|
||||
|
||||
func (a ByName) Len() int { return len(a) }
|
||||
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByName) Less(i, j int) bool {
|
||||
if a[i].Name == a[j].Name {
|
||||
return a[i].Value < a[j].Value
|
||||
}
|
||||
|
||||
return a[i].Name < a[j].Name
|
||||
}
|
||||
|
||||
func headerToReqHeader(headers http.Header) (reqHeaders []HTTPHeader) {
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
reqHeaders = append(reqHeaders, HTTPHeader{Name: name, Value: value})
|
||||
}
|
||||
}
|
||||
|
||||
return reqHeaders
|
||||
}
|
||||
|
||||
func TestSerializeNoHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
+6
-6
@@ -40,8 +40,6 @@ type HTTP2Connection struct {
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndex uint8
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
log *zerolog.Logger
|
||||
activeRequestsWG sync.WaitGroup
|
||||
@@ -69,7 +67,6 @@ func NewHTTP2Connection(
|
||||
connOptions: connOptions,
|
||||
observer: observer,
|
||||
connIndex: connIndex,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
controlStreamHandler: controlStreamHandler,
|
||||
log: log,
|
||||
}
|
||||
@@ -142,7 +139,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, respWriter, r)
|
||||
requestErr = originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
Dest: host,
|
||||
CFRay: FindCfRayHeader(r),
|
||||
@@ -289,6 +286,10 @@ func (rp *http2RespWriter) Header() http.Header {
|
||||
return rp.respHeaders
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) Flush() {
|
||||
rp.flusher.Flush()
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteHeader(status int) {
|
||||
if rp.hijacked() {
|
||||
rp.log.Warn().Msg("WriteHeader after hijack")
|
||||
@@ -384,8 +385,7 @@ func determineHTTP2Type(r *http.Request) Type {
|
||||
func handleMissingRequestParts(connType Type, r *http.Request) {
|
||||
if connType == TypeHTTP {
|
||||
// http library has no guarantees that we receive a filled URL. If not, then we fill it, as we reuse the request
|
||||
// for proxying. We use the same values as we used to in h2mux. For proxying they should not matter since we
|
||||
// control the dialer on every egress proxied.
|
||||
// for proxying. For proxying they should not matter since we control the dialer on every egress proxied.
|
||||
if len(r.URL.Scheme) == 0 {
|
||||
r.URL.Scheme = "http"
|
||||
}
|
||||
|
||||
+23
-19
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -21,8 +20,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -37,10 +36,11 @@ func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelProperties{},
|
||||
&TunnelProperties{},
|
||||
connIndex,
|
||||
nil,
|
||||
nil,
|
||||
1*time.Second,
|
||||
nil,
|
||||
1*time.Second,
|
||||
HTTP2,
|
||||
@@ -84,7 +84,7 @@ func TestHTTP2ConfigurationSet(t *testing.T) {
|
||||
resp, err := edgeHTTP2Conn.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
bdy, err := ioutil.ReadAll(resp.Body)
|
||||
bdy, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy))
|
||||
cancel()
|
||||
@@ -149,7 +149,7 @@ func TestServeHTTP(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedStatus, resp.StatusCode)
|
||||
if test.expectedBody != nil {
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedBody, respBody)
|
||||
}
|
||||
@@ -169,31 +169,32 @@ type mockNamedTunnelRPCClient struct {
|
||||
unregistered chan struct{}
|
||||
}
|
||||
|
||||
func (mc mockNamedTunnelRPCClient) SendLocalConfiguration(c context.Context, config []byte, observer *Observer) error {
|
||||
func (mc mockNamedTunnelRPCClient) SendLocalConfiguration(c context.Context, config []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mc mockNamedTunnelRPCClient) RegisterConnection(
|
||||
c context.Context,
|
||||
properties *NamedTunnelProperties,
|
||||
options *tunnelpogs.ConnectionOptions,
|
||||
ctx context.Context,
|
||||
auth pogs.TunnelAuth,
|
||||
tunnelID uuid.UUID,
|
||||
options *pogs.ConnectionOptions,
|
||||
connIndex uint8,
|
||||
edgeAddress net.IP,
|
||||
observer *Observer,
|
||||
) (*tunnelpogs.ConnectionDetails, error) {
|
||||
) (*pogs.ConnectionDetails, error) {
|
||||
if mc.shouldFail != nil {
|
||||
return nil, mc.shouldFail
|
||||
}
|
||||
close(mc.registered)
|
||||
return &tunnelpogs.ConnectionDetails{
|
||||
return &pogs.ConnectionDetails{
|
||||
Location: "LIS",
|
||||
UUID: uuid.New(),
|
||||
TunnelIsRemotelyManaged: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) {
|
||||
func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) error {
|
||||
close(mc.unregistered)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mockNamedTunnelRPCClient) Close() {}
|
||||
@@ -204,8 +205,8 @@ type mockRPCClientFactory struct {
|
||||
unregistered chan struct{}
|
||||
}
|
||||
|
||||
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient {
|
||||
return mockNamedTunnelRPCClient{
|
||||
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient {
|
||||
return &mockNamedTunnelRPCClient{
|
||||
shouldFail: mf.shouldFail,
|
||||
registered: mf.registered,
|
||||
unregistered: mf.unregistered,
|
||||
@@ -361,10 +362,11 @@ func TestServeControlStream(t *testing.T) {
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelProperties{},
|
||||
&TunnelProperties{},
|
||||
1,
|
||||
nil,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
1*time.Second,
|
||||
nil,
|
||||
1*time.Second,
|
||||
HTTP2,
|
||||
@@ -413,10 +415,11 @@ func TestFailRegistration(t *testing.T) {
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelProperties{},
|
||||
&TunnelProperties{},
|
||||
http2Conn.connIndex,
|
||||
nil,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
1*time.Second,
|
||||
nil,
|
||||
1*time.Second,
|
||||
HTTP2,
|
||||
@@ -461,10 +464,11 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelProperties{},
|
||||
&TunnelProperties{},
|
||||
http2Conn.connIndex,
|
||||
nil,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
1*time.Second,
|
||||
shutdownC,
|
||||
1*time.Second,
|
||||
HTTP2,
|
||||
@@ -546,7 +550,7 @@ func benchmarkServeHTTP(b *testing.B, test testRequest) {
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, test.expectedStatus, resp.StatusCode)
|
||||
if test.expectedBody != nil {
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, test.expectedBody, respBody)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@ package connection
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,34 +13,12 @@ const (
|
||||
configSubsystem = "config"
|
||||
)
|
||||
|
||||
type muxerMetrics struct {
|
||||
rtt *prometheus.GaugeVec
|
||||
rttMin *prometheus.GaugeVec
|
||||
rttMax *prometheus.GaugeVec
|
||||
receiveWindowAve *prometheus.GaugeVec
|
||||
sendWindowAve *prometheus.GaugeVec
|
||||
receiveWindowMin *prometheus.GaugeVec
|
||||
receiveWindowMax *prometheus.GaugeVec
|
||||
sendWindowMin *prometheus.GaugeVec
|
||||
sendWindowMax *prometheus.GaugeVec
|
||||
inBoundRateCurr *prometheus.GaugeVec
|
||||
inBoundRateMin *prometheus.GaugeVec
|
||||
inBoundRateMax *prometheus.GaugeVec
|
||||
outBoundRateCurr *prometheus.GaugeVec
|
||||
outBoundRateMin *prometheus.GaugeVec
|
||||
outBoundRateMax *prometheus.GaugeVec
|
||||
compBytesBefore *prometheus.GaugeVec
|
||||
compBytesAfter *prometheus.GaugeVec
|
||||
compRateAve *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
type localConfigMetrics struct {
|
||||
pushes prometheus.Counter
|
||||
pushesErrors prometheus.Counter
|
||||
}
|
||||
|
||||
type tunnelMetrics struct {
|
||||
timerRetries prometheus.Gauge
|
||||
serverLocations *prometheus.GaugeVec
|
||||
// locationLock is a mutex for oldServerLocations
|
||||
locationLock sync.Mutex
|
||||
@@ -54,7 +29,6 @@ type tunnelMetrics struct {
|
||||
regFail *prometheus.CounterVec
|
||||
rpcFail *prometheus.CounterVec
|
||||
|
||||
muxerMetrics *muxerMetrics
|
||||
tunnelsHA tunnelsForHA
|
||||
userHostnamesCounts *prometheus.CounterVec
|
||||
|
||||
@@ -92,252 +66,6 @@ func newLocalConfigMetrics() *localConfigMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
func newMuxerMetrics() *muxerMetrics {
|
||||
rtt := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt",
|
||||
Help: "Round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(rtt)
|
||||
|
||||
rttMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt_min",
|
||||
Help: "Shortest round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(rttMin)
|
||||
|
||||
rttMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "rtt_max",
|
||||
Help: "Longest round-trip time in millisecond",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(rttMax)
|
||||
|
||||
receiveWindowAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_ave",
|
||||
Help: "Average receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(receiveWindowAve)
|
||||
|
||||
sendWindowAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_ave",
|
||||
Help: "Average send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(sendWindowAve)
|
||||
|
||||
receiveWindowMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_min",
|
||||
Help: "Smallest receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(receiveWindowMin)
|
||||
|
||||
receiveWindowMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "receive_window_max",
|
||||
Help: "Largest receive window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(receiveWindowMax)
|
||||
|
||||
sendWindowMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_min",
|
||||
Help: "Smallest send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(sendWindowMin)
|
||||
|
||||
sendWindowMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "send_window_max",
|
||||
Help: "Largest send window size in bytes",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(sendWindowMax)
|
||||
|
||||
inBoundRateCurr := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_curr",
|
||||
Help: "Current inbounding bytes per second, 0 if there is no incoming connection",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(inBoundRateCurr)
|
||||
|
||||
inBoundRateMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero inbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(inBoundRateMin)
|
||||
|
||||
inBoundRateMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "inbound_bytes_per_sec_max",
|
||||
Help: "Maximum inbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(inBoundRateMax)
|
||||
|
||||
outBoundRateCurr := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_curr",
|
||||
Help: "Current outbounding bytes per second, 0 if there is no outgoing traffic",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(outBoundRateCurr)
|
||||
|
||||
outBoundRateMin := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_min",
|
||||
Help: "Minimum non-zero outbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(outBoundRateMin)
|
||||
|
||||
outBoundRateMax := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "outbound_bytes_per_sec_max",
|
||||
Help: "Maximum outbounding bytes per second",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(outBoundRateMax)
|
||||
|
||||
compBytesBefore := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_bytes_before",
|
||||
Help: "Bytes sent via cross-stream compression, pre compression",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(compBytesBefore)
|
||||
|
||||
compBytesAfter := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_bytes_after",
|
||||
Help: "Bytes sent via cross-stream compression, post compression",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(compBytesAfter)
|
||||
|
||||
compRateAve := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: muxerSubsystem,
|
||||
Name: "comp_rate_ave",
|
||||
Help: "Average outbound cross-stream compression ratio",
|
||||
},
|
||||
[]string{"connection_id"},
|
||||
)
|
||||
prometheus.MustRegister(compRateAve)
|
||||
|
||||
return &muxerMetrics{
|
||||
rtt: rtt,
|
||||
rttMin: rttMin,
|
||||
rttMax: rttMax,
|
||||
receiveWindowAve: receiveWindowAve,
|
||||
sendWindowAve: sendWindowAve,
|
||||
receiveWindowMin: receiveWindowMin,
|
||||
receiveWindowMax: receiveWindowMax,
|
||||
sendWindowMin: sendWindowMin,
|
||||
sendWindowMax: sendWindowMax,
|
||||
inBoundRateCurr: inBoundRateCurr,
|
||||
inBoundRateMin: inBoundRateMin,
|
||||
inBoundRateMax: inBoundRateMax,
|
||||
outBoundRateCurr: outBoundRateCurr,
|
||||
outBoundRateMin: outBoundRateMin,
|
||||
outBoundRateMax: outBoundRateMax,
|
||||
compBytesBefore: compBytesBefore,
|
||||
compBytesAfter: compBytesAfter,
|
||||
compRateAve: compRateAve,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *muxerMetrics) update(connectionID string, metrics *h2mux.MuxerMetrics) {
|
||||
m.rtt.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTT))
|
||||
m.rttMin.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTTMin))
|
||||
m.rttMax.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTTMax))
|
||||
m.receiveWindowAve.WithLabelValues(connectionID).Set(metrics.ReceiveWindowAve)
|
||||
m.sendWindowAve.WithLabelValues(connectionID).Set(metrics.SendWindowAve)
|
||||
m.receiveWindowMin.WithLabelValues(connectionID).Set(float64(metrics.ReceiveWindowMin))
|
||||
m.receiveWindowMax.WithLabelValues(connectionID).Set(float64(metrics.ReceiveWindowMax))
|
||||
m.sendWindowMin.WithLabelValues(connectionID).Set(float64(metrics.SendWindowMin))
|
||||
m.sendWindowMax.WithLabelValues(connectionID).Set(float64(metrics.SendWindowMax))
|
||||
m.inBoundRateCurr.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateCurr))
|
||||
m.inBoundRateMin.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateMin))
|
||||
m.inBoundRateMax.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateMax))
|
||||
m.outBoundRateCurr.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateCurr))
|
||||
m.outBoundRateMin.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMin))
|
||||
m.outBoundRateMax.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMax))
|
||||
m.compBytesBefore.WithLabelValues(connectionID).Set(float64(metrics.CompBytesBefore.Value()))
|
||||
m.compBytesAfter.WithLabelValues(connectionID).Set(float64(metrics.CompBytesAfter.Value()))
|
||||
m.compRateAve.WithLabelValues(connectionID).Set(float64(metrics.CompRateAve()))
|
||||
}
|
||||
|
||||
func convertRTTMilliSec(t time.Duration) float64 {
|
||||
return float64(t / time.Millisecond)
|
||||
}
|
||||
|
||||
// Metrics that can be collected without asking the edge
|
||||
func initTunnelMetrics() *tunnelMetrics {
|
||||
maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec(
|
||||
@@ -351,15 +79,6 @@ func initTunnelMetrics() *tunnelMetrics {
|
||||
)
|
||||
prometheus.MustRegister(maxConcurrentRequestsPerTunnel)
|
||||
|
||||
timerRetries := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: TunnelSubsystem,
|
||||
Name: "timer_retries",
|
||||
Help: "Unacknowledged heart beats count",
|
||||
})
|
||||
prometheus.MustRegister(timerRetries)
|
||||
|
||||
serverLocations := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
@@ -416,10 +135,8 @@ func initTunnelMetrics() *tunnelMetrics {
|
||||
prometheus.MustRegister(registerSuccess)
|
||||
|
||||
return &tunnelMetrics{
|
||||
timerRetries: timerRetries,
|
||||
serverLocations: serverLocations,
|
||||
oldServerLocations: make(map[string]string),
|
||||
muxerMetrics: newMuxerMetrics(),
|
||||
tunnelsHA: newTunnelsForHA(),
|
||||
regSuccess: registerSuccess,
|
||||
regFail: registerFail,
|
||||
@@ -429,10 +146,6 @@ func initTunnelMetrics() *tunnelMetrics {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tunnelMetrics) updateMuxerMetrics(connectionID string, metrics *h2mux.MuxerMetrics) {
|
||||
t.muxerMetrics.update(connectionID, metrics)
|
||||
}
|
||||
|
||||
func (t *tunnelMetrics) registerServerLocation(connectionID, loc string) {
|
||||
t.locationLock.Lock()
|
||||
defer t.locationLock.Unlock()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user