mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ecb6d3e88 | |||
| 88c25d2c67 | |||
| 38cd455e4d | |||
| ee5e447d44 | |||
| 4d30a71434 | |||
| 39b7aed24e | |||
| 4de1bc4bba | |||
| e426693330 | |||
| 0b5b9b8297 | |||
| 7a014b06ec | |||
| 171d4ac77c | |||
| 5e212a6bf3 | |||
| 3996b1adca | |||
| 71997be90e | |||
| 991f01fe34 | |||
| b89c092c1b | |||
| 8dc0697a8f | |||
| 5dbf76a7aa | |||
| 8d87d4facd | |||
| 3fd571063e | |||
| 5d0bb25572 | |||
| c51b651afb | |||
| 04367b0f63 | |||
| 69eb9698b5 | |||
| 55ed995bf0 | |||
| 820a201603 | |||
| 93acdaface | |||
| 5972540efa | |||
| 5e37a65dac | |||
| bfbe426905 | |||
| 39ed5dc182 | |||
| bbc8d9431b | |||
| b5e03dd66c | |||
| 87f81cc57c | |||
| be64362fdb | |||
| f686da832f | |||
| be341fa055 | |||
| ec2d18ea4f | |||
| 1742379ba4 | |||
| 9c15f31d00 | |||
| 53fb50960d | |||
| 7b8b3f73e7 | |||
| ede3c8e056 | |||
| 93f8f6b55c | |||
| bf3136debb | |||
| 27f88ae209 | |||
| 7080b8b2e6 | |||
| 4c3417fedd | |||
| 354281fc6a | |||
| b6d1daaf20 | |||
| 844b4938ca | |||
| fed60ae4c3 | |||
| b97979487e | |||
| 2221325f3d | |||
| 2bb054c4bf | |||
| 68ef4ab2a8 | |||
| ea6fe121f8 | |||
| 079631ccea | |||
| 8cf2d319ca | |||
| 0f95f8bae5 | |||
| ae46af9236 | |||
| bd046677e5 | |||
| 8a9f076a26 | |||
| 62dcb8a1d1 | |||
| 90d710e3ec | |||
| b8e610a067 | |||
| c24f275981 | |||
| d8f2b768f8 | |||
| 93e569fa23 | |||
| 207f4e2c8d | |||
| 513855df5c | |||
| bd917d294c | |||
| 4616e9fcc2 | |||
| de7ca4be30 | |||
| 4d993488df | |||
| 794e8e622f | |||
| 87bd36c924 | |||
| de4fd472f3 | |||
| 887e486a63 | |||
| 645e22744c | |||
| d19da6767a | |||
| 045439f0ab | |||
| 2519aec733 | |||
| 99b3736cc7 | |||
| e517242194 | |||
| 7dee179652 | |||
| 78ca8002d2 | |||
| c13b6df0a7 | |||
| b8b35d99fa | |||
| 61ccc0b303 | |||
| 7ef9bb89d3 | |||
| 45e8eb7275 | |||
| 72503eeaaa | |||
| 09e33a0b17 | |||
| 4c10f68e2d | |||
| cf87ec7969 | |||
| 64f15d9992 | |||
| e3d35570e6 | |||
| b0663dce33 | |||
| af59851f33 | |||
| c49621c723 | |||
| 9339bb9485 | |||
| 19106cd609 | |||
| b50f172bdb | |||
| 1c6316c1c9 | |||
| 1fe4878264 | |||
| 85b44695f0 | |||
| 6a1dad0ce2 | |||
| 2baea15387 | |||
| a1d88a6cdd | |||
| 515ad7cbee | |||
| 1b5313cc28 | |||
| dde83d5a7c | |||
| e14238224d | |||
| 66d1f27507 | |||
| e6c9ec0b39 | |||
| c3c050aa79 | |||
| b1de2a74fa | |||
| 4d32a64f98 | |||
| 11f4d10174 | |||
| 60a12fcb27 | |||
| 442af9ee38 | |||
| 2e895c3a4f | |||
| e9d07e35c7 | |||
| 2d5234e021 | |||
| b6bd8c1f5e | |||
| 495f9fb8bd | |||
| 225c344ceb | |||
| 61007dd2dd | |||
| b01006fe46 | |||
| 872cb003a4 | |||
| 2aca844570 | |||
| 90e5255a0d | |||
| 4aead129ed | |||
| 9904929b83 | |||
| c280d62fe5 | |||
| 40ea6a5080 | |||
| 4642316167 | |||
| d0c10b34dd | |||
| f4ae8d1446 | |||
| e89bceca5e | |||
| 6be36fa2c5 | |||
| f81d35447e | |||
| 49438f30f5 | |||
| eacc8c648d | |||
| 5b30925773 | |||
| d7fb18be22 | |||
| cbf8c71fab | |||
| 870193c064 | |||
| fdddd86380 | |||
| b3e26420c0 | |||
| be0305ec58 | |||
| 3449ea35f2 | |||
| 7f487c2651 | |||
| 9bb7628fbc | |||
| eb36716ba4 | |||
| 5d6b0642db | |||
| 462d2f87df | |||
| 0aa21f302e | |||
| de07da02cd | |||
| e9a2c85671 | |||
| b0f0741a9b | |||
| db4564e5b9 | |||
| 3d345d3748 | |||
| b1995b4dd1 | |||
| b457cca1e5 | |||
| a0b6ba9b8d | |||
| de00396669 | |||
| 013bdbd10c | |||
| b639b6627a | |||
| e454994e3e | |||
| 8a53c1aa1d | |||
| f5f3e6a453 | |||
| 30c529e730 | |||
| bf3d70d1d2 | |||
| a65f8bce7f | |||
| 2ffff0687b | |||
| e380333520 | |||
| 11cbff4ff7 | |||
| 3e0ff3a771 | |||
| 7a19798682 | |||
| 4b75943d59 | |||
| fc20a22685 | |||
| faa86ffeca | |||
| f7a14d9200 | |||
| 902e5beb4f | |||
| 7ca5f7569a | |||
| 4ac68711cd | |||
| 075ac1acf1 | |||
| cfef0e737f | |||
| 8ec0f7746b | |||
| 2b3707e2b9 | |||
| 7e760f9fcc | |||
| efb99d90d7 | |||
| e131125558 | |||
| af6bf5c4e5 | |||
| e3390fcb15 | |||
| fc5749328d | |||
| 59f5b0df83 | |||
| f6bd4aa039 | |||
| d2bc15e224 | |||
| bad2e8e812 | |||
| 20ed7557f9 | |||
| 8e9e1d973e | |||
| a97673e8b9 |
+1
-2
@@ -2,8 +2,7 @@ images:
|
||||
- name: cloudflared
|
||||
dockerfile: Dockerfile.$ARCH
|
||||
context: .
|
||||
versions:
|
||||
- latest
|
||||
version_file: versions
|
||||
registries:
|
||||
- name: docker.io/cloudflare
|
||||
user: env:DOCKER_USER
|
||||
|
||||
@@ -15,6 +15,10 @@ Steps to reproduce the behavior:
|
||||
2. Run '....'
|
||||
3. See error
|
||||
|
||||
If it's an issue with Cloudflare Tunnel:
|
||||
4. Tunnel ID :
|
||||
5. cloudflared config:
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ jobs:
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.x]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
Vendored
+11
-14
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -exo pipefail
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]] ; then
|
||||
echo "This should be run on macOS"
|
||||
exit 1
|
||||
@@ -33,7 +35,9 @@ if [[ ! -z "$CFD_CODE_SIGN_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
|
||||
# write private key to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_KEY} | base64 -D > ${CODE_SIGN_PRIV}
|
||||
out=$(security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}" 2>&1)
|
||||
# we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error
|
||||
# will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below.
|
||||
out=$(security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}" 2>&1) || true
|
||||
exitcode=$?
|
||||
if [ -n "$out" ]; then
|
||||
if [ $exitcode -eq 0 ]; then
|
||||
@@ -53,7 +57,7 @@ fi
|
||||
if [[ ! -z "$CFD_CODE_SIGN_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_CODE_SIGN_CERT} | base64 -D > ${CODE_SIGN_CERT}
|
||||
out1=$(security import ${CODE_SIGN_CERT} -A 2>&1)
|
||||
out1=$(security import ${CODE_SIGN_CERT} -A 2>&1) || true
|
||||
exitcode1=$?
|
||||
if [ -n "$out1" ]; then
|
||||
if [ $exitcode1 -eq 0 ]; then
|
||||
@@ -75,7 +79,7 @@ if [[ ! -z "$CFD_INSTALLER_KEY" ]]; then
|
||||
if [[ ! -z "$CFD_INSTALLER_PASS" ]]; then
|
||||
# write private key to disk and then import it into the keychain
|
||||
echo -n -e ${CFD_INSTALLER_KEY} | base64 -D > ${INSTALLER_PRIV}
|
||||
out2=$(security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}" 2>&1)
|
||||
out2=$(security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}" 2>&1) || true
|
||||
exitcode2=$?
|
||||
if [ -n "$out2" ]; then
|
||||
if [ $exitcode2 -eq 0 ]; then
|
||||
@@ -95,7 +99,7 @@ fi
|
||||
if [[ ! -z "$CFD_INSTALLER_CERT" ]]; then
|
||||
# write certificate to disk and then import it keychain
|
||||
echo -n -e ${CFD_INSTALLER_CERT} | base64 -D > ${INSTALLER_CERT}
|
||||
out3=$(security import ${INSTALLER_CERT} -A 2>&1)
|
||||
out3=$(security import ${INSTALLER_CERT} -A 2>&1) || true
|
||||
exitcode3=$?
|
||||
if [ -n "$out3" ]; then
|
||||
if [ $exitcode3 -eq 0 ]; then
|
||||
@@ -138,14 +142,10 @@ fi
|
||||
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
|
||||
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
|
||||
|
||||
# notarize the binary
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
zip "${BINARY_NAME}.zip" ${BINARY_NAME}
|
||||
xcrun altool --notarize-app -f "${BINARY_NAME}.zip" -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
fi
|
||||
# notarize the binary
|
||||
# TODO: https://jira.cfdata.org/browse/TUN-5789
|
||||
fi
|
||||
|
||||
|
||||
# creating build directory
|
||||
rm -rf $TARGET_DIRECTORY
|
||||
mkdir "${TARGET_DIRECTORY}"
|
||||
@@ -169,10 +169,7 @@ if [[ ! -z "$PKG_SIGN_NAME" ]]; then
|
||||
${PKGNAME}
|
||||
|
||||
# notarize the package
|
||||
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
|
||||
xcrun altool --notarize-app -f ${PKGNAME} -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
|
||||
xcrun stapler staple ${PKGNAME}
|
||||
fi
|
||||
# TODO: https://jira.cfdata.org/browse/TUN-5789
|
||||
else
|
||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||
--version ${VERSION} \
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
export TARGET_OS=windows
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $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
|
||||
make cloudflared-msi
|
||||
# Copy msi into final directory
|
||||
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
|
||||
done
|
||||
Vendored
+1
-1
@@ -23,4 +23,4 @@ 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
|
||||
brew bump-formula-pr cloudflared --version="$VERSION" --no-browse --no-audit
|
||||
|
||||
Vendored
+5
-6
@@ -3,7 +3,6 @@
|
||||
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']"
|
||||
@@ -15,24 +14,24 @@ if [[ ! -f "$FILENAME" ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${GITHUB_PRIVATE_KEY:-}" == "" ]] ; then
|
||||
echo "Missing GITHUB_PRIVATE_KEY"
|
||||
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 --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
--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 --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
||||
--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" > tmp/private.key
|
||||
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"
|
||||
|
||||
|
||||
+39
@@ -1,3 +1,42 @@
|
||||
## 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.
|
||||
|
||||
## 2023.3.2
|
||||
### Notices
|
||||
- Due to the nature of QuickTunnels (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/do-more-with-tunnels/trycloudflare/) and its intended usage for testing and experiment of Cloudflare Tunnels, starting from 2023.3.2, QuickTunnels only make a single connection to the edge. If users want to use Tunnels in a production environment, they should move to Named Tunnels instead. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/remote/#set-up-a-tunnel-remotely-dashboard-setup)
|
||||
|
||||
## 2023.3.1
|
||||
### Breaking Change
|
||||
- Running a tunnel without ingress rules defined in configuration file nor from the CLI flags will no longer provide a default ingress rule to localhost:8080 and instead will return HTTP response code 503 for all incoming HTTP requests.
|
||||
|
||||
### Security Fixes
|
||||
- Windows 32 bit machines MSI now defaults to Program Files to install cloudflared. (See CVE-2023-1314). The cloudflared client itself is unaffected. This just changes how the installer works on 32 bit windows machines.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed a bug that would cause running tunnel on Bastion mode and without ingress rules to crash.
|
||||
|
||||
## 2023.2.2
|
||||
### Notices
|
||||
- Legacy tunnels were officially deprecated on December 1, 2022. Starting with this version, cloudflared no longer supports connecting legacy tunnels.
|
||||
- h2mux tunnel connection protocol is no longer supported. Any tunnels still configured to use this protocol will alert and use http2 tunnel protocol instead. We recommend using quic protocol for all tunnels going forward.
|
||||
|
||||
## 2023.2.1
|
||||
### Bug fixes
|
||||
- Fixed a bug in TCP connection proxy that could result in the connection being closed before all data was written.
|
||||
- cloudflared now correctly aborts body write if connection to origin service fails after response headers were sent already.
|
||||
- Fixed a bug introduced in the previous release where debug endpoints were removed.
|
||||
|
||||
## 2022.12.0
|
||||
### Improvements
|
||||
- cloudflared now attempts to try other edge addresses before falling back to a lower protocol.
|
||||
- cloudflared tunnel no longer spins up a quick tunnel. The call has to be explicit and provide a --url flag.
|
||||
- cloudflared will now randomly pick the first or second region to connect to instead of always connecting to region2 first.
|
||||
|
||||
## 2022.9.0
|
||||
### New Features
|
||||
- cloudflared now rejects ingress rules with invalid http status codes for http_status.
|
||||
|
||||
## 2022.8.1
|
||||
### New Features
|
||||
- cloudflared now remembers if it connected to a certain protocol successfully. If it did, it does not fall back to a lower
|
||||
|
||||
+2
-2
@@ -7,8 +7,6 @@ ENV GO111MODULE=on \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
TARGET_GOARCH=${TARGET_GOARCH}
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
@@ -20,6 +18,8 @@ RUN make cloudflared
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
|
||||
+2
-2
@@ -3,8 +3,6 @@ FROM golang:1.19 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
@@ -16,6 +14,8 @@ RUN GOOS=linux GOARCH=amd64 make cloudflared
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
|
||||
+2
-2
@@ -3,8 +3,6 @@ FROM golang:1.19 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
# copy our sources into the builder image
|
||||
@@ -16,6 +14,8 @@ RUN GOOS=linux GOARCH=arm64 make cloudflared
|
||||
# use a distroless base image with glibc
|
||||
FROM gcr.io/distroless/base-debian11:nonroot-arm64
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
|
||||
|
||||
# copy our compiled binary
|
||||
COPY --from=builder --chown=nonroot /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ else ifeq ($(LOCAL_OS),windows)
|
||||
TARGET_OS ?= windows
|
||||
else ifeq ($(LOCAL_OS),freebsd)
|
||||
TARGET_OS ?= freebsd
|
||||
else ifeq ($(LOCAL_OS),openbsd)
|
||||
TARGET_OS ?= openbsd
|
||||
else
|
||||
$(error This system's OS $(LOCAL_OS) isn't supported)
|
||||
endif
|
||||
@@ -108,6 +110,9 @@ else
|
||||
PACKAGE_ARCH := $(TARGET_ARCH)
|
||||
endif
|
||||
|
||||
#for FIPS compliance, FPM defaults to MD5.
|
||||
RPM_DIGEST := --rpm-digest sha256
|
||||
|
||||
.PHONY: all
|
||||
all: cloudflared test
|
||||
|
||||
@@ -131,6 +136,10 @@ endif
|
||||
container:
|
||||
docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" .
|
||||
|
||||
.PHONY: generate-docker-version
|
||||
generate-docker-version:
|
||||
echo latest $(VERSION) > versions
|
||||
|
||||
.PHONY: test
|
||||
test: vet
|
||||
ifndef CI
|
||||
@@ -145,24 +154,8 @@ endif
|
||||
test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
define publish_package
|
||||
chmod 664 $(BINARY_NAME)*.$(1); \
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t ecdsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -p -4 $(BINARY_NAME)*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/$(BINARY_NAME)/; \
|
||||
done
|
||||
endef
|
||||
|
||||
.PHONY: publish-deb
|
||||
publish-deb: cloudflared-deb
|
||||
$(call publish_package,deb,apt)
|
||||
|
||||
.PHONY: publish-rpm
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,rpm,yum)
|
||||
|
||||
cloudflared.1: cloudflared_man_template
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > cloudflared.1
|
||||
sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' cloudflared_man_template > cloudflared.1
|
||||
|
||||
install: cloudflared cloudflared.1
|
||||
mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR)
|
||||
@@ -175,13 +168,13 @@ define build_package
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
cp cloudflared.1 $(PACKAGE_DIR)/cloudflared.1
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
|
||||
fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
|
||||
--description 'Cloudflare Tunnel daemon' \
|
||||
--vendor 'Cloudflare' \
|
||||
--license 'Apache License Version 2.0' \
|
||||
--url 'https://github.com/cloudflare/cloudflared' \
|
||||
-m 'Cloudflare <support@cloudflare.com>' \
|
||||
-a $(PACKAGE_ARCH) -v $(VERSION) -n $(DEB_PACKAGE_NAME) $(NIGHTLY_FLAGS) --after-install postinst.sh --after-remove postrm.sh \
|
||||
-a $(PACKAGE_ARCH) -v $(VERSION) -n $(DEB_PACKAGE_NAME) $(RPM_DIGEST) $(NIGHTLY_FLAGS) --after-install postinst.sh --after-remove postrm.sh \
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(INSTALL_MANDIR)
|
||||
endef
|
||||
|
||||
@@ -198,7 +191,7 @@ cloudflared-pkg: cloudflared cloudflared.1
|
||||
$(call build_package,osxpkg)
|
||||
|
||||
.PHONY: cloudflared-msi
|
||||
cloudflared-msi: cloudflared
|
||||
cloudflared-msi:
|
||||
wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
@@ -288,10 +281,17 @@ 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: 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:
|
||||
which capnp # https://capnproto.org/install.html
|
||||
which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go
|
||||
which capnpc-go # go install zombiezen.com/go/capnproto2/capnpc-go@latest
|
||||
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: quic-deps
|
||||
@@ -302,8 +302,8 @@ quic-deps:
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet -mod=vendor ./...
|
||||
go vet -v -mod=vendor github.com/cloudflare/cloudflared/...
|
||||
|
||||
.PHONY: goimports
|
||||
goimports:
|
||||
for d in $$(go list -mod=readonly -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -w $$d ; done
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
goimports -l -w -local github.com/cloudflare/cloudflared $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc)
|
||||
|
||||
@@ -25,12 +25,13 @@ routing), but for legacy reasons this requirement is still necessary:
|
||||
|
||||
## Installing `cloudflared`
|
||||
|
||||
Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases here on the `cloudflared` GitHub repository.
|
||||
Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases [here](https://github.com/cloudflare/cloudflared/releases) on the `cloudflared` GitHub repository.
|
||||
|
||||
* You can [install on macOS](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases)
|
||||
* 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)
|
||||
|
||||
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
|
||||
|
||||
+211
@@ -1,3 +1,214 @@
|
||||
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
|
||||
- 2023-04-12 TUN-7131: Add cloudflared log event to connection messages and enable streaming logs
|
||||
- 2023-04-11 TUN-7132 TUN-7136: Add filter support for streaming logs
|
||||
- 2023-04-06 TUN-7354: Don't warn for empty ingress rules when using --token
|
||||
- 2023-04-06 TUN-7128: Categorize logs from public hostname locations
|
||||
- 2023-04-06 TUN-7351: Add streaming logs session ping and timeout
|
||||
- 2023-04-06 TUN-7335: Fix cloudflared update not working in windows
|
||||
|
||||
2023.4.0
|
||||
- 2023-04-07 TUN-7356: Bump golang.org/x/net package to 0.7.0
|
||||
- 2023-04-07 TUN-7357: Bump to go 1.19.6
|
||||
- 2023-04-06 TUN-7127: Disconnect logger level requirement for management
|
||||
- 2023-04-05 TUN-7332: Remove legacy tunnel force flag
|
||||
- 2023-04-05 TUN-7135: Add cloudflared tail
|
||||
- 2023-04-04 Add suport for OpenBSD (#916)
|
||||
- 2023-04-04 Fix typo (#918)
|
||||
- 2023-04-04 TUN-7125: Add management streaming logs WebSocket protocol
|
||||
- 2023-03-30 TUN-9999: Remove classic tunnel component tests
|
||||
- 2023-03-30 TUN-7126: Add Management logger io.Writer
|
||||
- 2023-03-29 TUN-7324: Add http.Hijacker to connection.ResponseWriter
|
||||
- 2023-03-29 TUN-7333: Default features checkable at runtime across all packages
|
||||
- 2023-03-21 TUN-7124: Add intercept ingress rule for management requests
|
||||
|
||||
2023.3.1
|
||||
- 2023-03-13 TUN-7271: Return 503 status code when no ingress rules configured
|
||||
- 2023-03-10 TUN-7272: Fix cloudflared returning non supported status service which breaks configuration migration
|
||||
- 2023-03-09 TUN-7259: Add warning for missing ingress rules
|
||||
- 2023-03-09 TUN-7268: Default to Program Files as location for win32
|
||||
- 2023-03-07 TUN-7252: Remove h2mux connection
|
||||
- 2023-03-07 TUN-7253: Adopt http.ResponseWriter for connection.ResponseWriter
|
||||
- 2023-03-06 TUN-7245: Add bastion flag to origin service check
|
||||
- 2023-03-06 EDGESTORE-108: Remove deprecated s3v2 signature
|
||||
- 2023-03-02 TUN-7226: Fixed a missed rename
|
||||
|
||||
2023.3.0
|
||||
- 2023-03-01 GH-352: Add Tunnel CLI option "edge-bind-address" (#870)
|
||||
- 2023-03-01 Fixed WIX template to allow MSI upgrades (#838)
|
||||
- 2023-02-28 TUN-7213: Decode Base64 encoded key before writing it
|
||||
- 2023-02-28 check.yaml: update actions to v3 (#876)
|
||||
- 2023-02-27 TUN-7213: Debug homebrew-cloudflare build
|
||||
- 2023-02-15 RTG-2476 Add qtls override for Go 1.20
|
||||
|
||||
2023.2.2
|
||||
- 2023-02-22 TUN-7197: Add connIndex tag to debug messages of incoming requests
|
||||
- 2023-02-08 TUN-7167: Respect protocol overrides with --token
|
||||
- 2023-02-06 TUN-7065: Remove classic tunnel creation
|
||||
- 2023-02-06 TUN-6938: Force h2mux protocol to http2 for named tunnels
|
||||
- 2023-02-06 TUN-6938: Provide QUIC as first in protocol list
|
||||
- 2023-02-03 TUN-7158: Correct TCP tracing propagation
|
||||
- 2023-02-01 TUN-7151: Update changes file with latest release notices
|
||||
|
||||
2023.2.1
|
||||
- 2023-02-01 TUN-7065: Revert Ingress Rule check for named tunnel configurations
|
||||
- 2023-02-01 Revert "TUN-7065: Revert Ingress Rule check for named tunnel configurations"
|
||||
- 2023-02-01 Revert "TUN-7065: Remove classic tunnel creation"
|
||||
2023.1.0
|
||||
- 2023-01-10 TUN-7064: RPM digests are now sha256 instead of md5sum
|
||||
- 2023-01-04 RTG-2418 Update qtls
|
||||
- 2022-12-24 TUN-7057: Remove dependency github.com/gorilla/mux
|
||||
- 2022-12-24 TUN-6724: Migrate to sentry-go from raven-go
|
||||
|
||||
2022.12.1
|
||||
- 2022-12-20 TUN-7021: Fix proxy-dns not starting when cloudflared tunnel is run
|
||||
- 2022-12-15 TUN-7010: Changelog for release 2022.12.0
|
||||
|
||||
2022.12.0
|
||||
- 2022-12-14 TUN-6999: cloudflared should attempt other edge addresses before falling back on protocol
|
||||
- 2022-12-13 TUN-7004: Dont show local config dirs for remotely configured tuns
|
||||
- 2022-12-12 TUN-7003: Tempoarily disable erroneous notarize-app
|
||||
- 2022-12-12 TUN-7003: Add back a missing fi
|
||||
- 2022-12-07 TUN-7000: Reduce metric cardinality of closedConnections metric by removing error as tag
|
||||
- 2022-12-07 TUN-6994: Improve logging config file not found
|
||||
- 2022-12-07 TUN-7002: Randomise first region selection
|
||||
- 2022-12-07 TUN-6995: Disable quick-tunnels spin up by default
|
||||
- 2022-12-05 TUN-6984: Add bash set x to improve visibility during builds
|
||||
- 2022-12-05 TUN-6984: [CI] Ignore security import errors for code_sigining
|
||||
- 2022-12-05 TUN-6984: [CI] Don't fail on unset.
|
||||
- 2022-11-30 TUN-6984: Set euo pipefile for homebrew builds
|
||||
|
||||
2022.11.1
|
||||
- 2022-11-29 TUN-6981: We should close UDP socket if failed to connecto to edge
|
||||
- 2022-11-25 CUSTESC-23757: Fix a bug where a wildcard ingress rule would match an host without starting with a dot
|
||||
- 2022-11-24 TUN-6970: Print newline when printing tunnel token
|
||||
- 2022-11-22 TUN-6963: Refactor Metrics service setup
|
||||
2022.11.0
|
||||
- 2022-11-16 Revert "TUN-6935: Cloudflared should use APIToken instead of serviceKey"
|
||||
- 2022-11-16 TUN-6929: Use same protocol for other connections as first one
|
||||
- 2022-11-14 TUN-6941: Reduce log level to debug when failing to proxy ICMP reply
|
||||
- 2022-11-14 TUN-6935: Cloudflared should use APIToken instead of serviceKey
|
||||
- 2022-11-14 TUN-6935: Cloudflared should use APIToken instead of serviceKey
|
||||
- 2022-11-11 TUN-6937: Bump golang.org/x/* packages to new release tags
|
||||
- 2022-11-10 ZTC-234: macOS tests
|
||||
- 2022-11-09 TUN-6927: Refactor validate access configuration to allow empty audTags only
|
||||
- 2022-11-08 ZTC-234: Replace ICMP funnels when ingress connection changes
|
||||
- 2022-11-04 TUN-6917: Bump go to 1.19.3
|
||||
- 2022-11-02 Issue #574: Better ssh config for short-lived cert (#763)
|
||||
- 2022-10-28 TUN-6898: Fix bug handling IPv6 based ingresses with missing port
|
||||
- 2022-10-28 TUN-6898: Refactor addPortIfMissing
|
||||
2022.10.3
|
||||
- 2022-10-24 TUN-6871: Add default feature to cloudflared to support EOF on QUIC connections
|
||||
- 2022-10-19 TUN-6876: Fix flaky TestTraceICMPRouterEcho by taking account request span can return before reply
|
||||
- 2022-10-18 TUN-6867: Clear spans right after they are serialized to avoid returning duplicate spans
|
||||
2022.10.2
|
||||
- 2022-10-18 TUN-6869: Fix Makefile complaining about missing GO packages
|
||||
- 2022-10-18 TUN-6864: Don't reuse port in quic unit tests
|
||||
- 2022-10-18 TUN-6868: Return left padded tracing ID when tracing identity is converted to string
|
||||
|
||||
2022.10.1
|
||||
- 2022-10-16 TUN-6861: Trace ICMP on Windows
|
||||
- 2022-10-15 TUN-6860: Send access configuration keys to the edge
|
||||
- 2022-10-14 TUN-6858: Trace ICMP reply
|
||||
- 2022-10-13 TUN-6855: Add DatagramV2Type for IP packet with trace and tracing spans
|
||||
- 2022-10-13 TUN-6856: Refactor to lay foundation for tracing ICMP
|
||||
- 2022-10-13 TUN-6604: Trace icmp echo request on Linux and Darwin
|
||||
- 2022-10-12 Fix log message (#591)
|
||||
- 2022-10-12 TUN-6853: Reuse source port when connecting to the edge for quic connections
|
||||
- 2022-10-11 TUN-6829: Allow user of datagramsession to control logging level of errors
|
||||
- 2022-10-10 RTG-2276 Update qtls and go mod tidy
|
||||
- 2022-10-05 Add post-quantum flag to quick tunnel
|
||||
- 2022-10-05 TUN-6823: Update github release message to pull from KV
|
||||
- 2022-10-04 TUN-6825: Fix cloudflared:version images require arch hyphens
|
||||
- 2022-10-03 TUN-6806: Add ingress rule number to log when filtering due to middlware handler
|
||||
- 2022-08-17 Label correct container
|
||||
- 2022-08-16 Fix typo in help text for `cloudflared tunnel route lb`
|
||||
- 2022-07-18 drop usage of cat when sed is invoked to generate the manpage
|
||||
- 2021-03-15 update-build-readme
|
||||
- 2021-03-15 fix link
|
||||
|
||||
2022.10.0
|
||||
- 2022-09-30 TUN-6755: Remove unused publish functions
|
||||
- 2022-09-30 TUN-6813: Only proxy ICMP packets when warp-routing is enabled
|
||||
- 2022-09-29 TUN-6811: Ping group range should be parsed as int32
|
||||
- 2022-09-29 TUN-6812: Drop IP packets if ICMP proxy is not initialized
|
||||
- 2022-09-28 TUN-6716: Document limitation of Windows ICMP proxy
|
||||
- 2022-09-28 TUN-6810: Add component test for post-quantum
|
||||
- 2022-09-27 TUN-6715: Provide suggestion to add cloudflared to ping_group_range if it failed to open ICMP socket
|
||||
- 2022-09-22 TUN-6792: Fix brew core release by not auditing the formula
|
||||
- 2022-09-22 TUN-6774: Validate OriginRequest.Access to add Ingress.Middleware
|
||||
- 2022-09-22 TUN-6775: Add middleware.Handler verification to ProxyHTTP
|
||||
- 2022-09-22 TUN-6791: Calculate ICMPv6 checksum
|
||||
- 2022-09-22 TUN-6801: Add punycode alternatives for ingress rules
|
||||
- 2022-09-21 TUN-6772: Add a JWT Validator as an ingress verifier
|
||||
- 2022-09-21 TUN-6772: Add a JWT Validator as an ingress verifier
|
||||
- 2022-09-21 TUN-6774: Validate OriginRequest.Access to add Ingress.Middleware
|
||||
- 2022-09-21 TUN-6772: Add a JWT Validator as an ingress verifier
|
||||
- 2022-09-20 TUN-6741: ICMP proxy tries to listen on specific IPv4 & IPv6 when possible
|
||||
2022.9.1
|
||||
- 2022-09-20 TUN-6777: Fix race condition in TestFunnelIdleTimeout
|
||||
- 2022-09-20 TUN-6595: Enable datagramv2 and icmp proxy by default
|
||||
- 2022-09-20 TUN-6773: Add access based configuration to ingress.OriginRequestConfig
|
||||
- 2022-09-19 TUN-6778: Cleanup logs about ICMP
|
||||
- 2022-09-19 TUN-6779: cloudflared should also use the root CAs from system pool to validate edge certificate
|
||||
- 2022-09-19 TUN-6780: Add support for certReload to also include support for client certificates
|
||||
- 2022-09-16 TUN-6767: Build ICMP proxy for Windows only when CGO is enabled
|
||||
- 2022-09-15 TUN-6590: Use Windows Teamcity agent to build binary
|
||||
- 2022-09-13 TUN-6592: Decrement TTL and return ICMP time exceed if it's 0
|
||||
- 2022-09-09 TUN-6749: Fix icmp_generic build
|
||||
- 2022-09-09 TUN-6744: On posix platforms, assign unique echo ID per (src, dst, echo ID)
|
||||
- 2022-09-08 TUN-6743: Support ICMPv6 echo on Windows
|
||||
- 2022-09-08 TUN-6689: Utilize new RegisterUDPSession to begin tracing
|
||||
- 2022-09-07 TUN-6688: Update RegisterUdpSession capnproto to include trace context
|
||||
- 2022-09-06 TUN-6740: Detect no UDP packets allowed and fallback from QUIC in that case
|
||||
- 2022-09-06 TUN-6654: Support ICMPv6 on Linux and Darwin
|
||||
- 2022-09-02 TUN-6696: Refactor flow into funnel and close idle funnels
|
||||
- 2022-09-02 TUN-6718: Bump go and go-boring 1.18.6
|
||||
- 2022-08-29 TUN-6531: Implement ICMP proxy for Windows using IcmpSendEcho
|
||||
- 2022-08-24 RTG-1339 Support post-quantum hybrid key exchange
|
||||
|
||||
2022.9.0
|
||||
- 2022-09-05 TUN-6737: Fix datagramV2Type should be declared in its own block so it starts at 0
|
||||
- 2022-09-01 TUN-6725: Fix testProxySSEAllData
|
||||
- 2022-09-01 TUN-6726: Fix maxDatagramPayloadSize for Windows QUIC datagrams
|
||||
- 2022-09-01 TUN-6729: Fix flaky TestClosePreviousProxies
|
||||
- 2022-09-01 TUN-6728: Verify http status code ingress rule
|
||||
- 2022-08-25 TUN-6695: Implement ICMP proxy for linux
|
||||
|
||||
2022.8.4
|
||||
- 2022-08-31 TUN-6717: Update Github action to run with Go 1.19
|
||||
- 2022-08-31 TUN-6720: Remove forcibly closing connection during reconnect signal
|
||||
- 2022-08-29 Release 2022.8.3
|
||||
|
||||
2022.8.3
|
||||
- 2022-08-26 TUN-6708: Fix replace flow logic
|
||||
- 2022-08-25 TUN-6705: Tunnel should retry connections forever
|
||||
- 2022-08-25 TUN-6704: Honor protocol flag when edge discovery is unreachable
|
||||
- 2022-08-25 TUN-6699: Add metric for packet too big dropped
|
||||
- 2022-08-24 TUN-6691: Properly error check for net.ErrClosed
|
||||
- 2022-08-22 TUN-6679: Allow client side of quic request to close body
|
||||
- 2022-08-22 TUN-6586: Change ICMP proxy to only build for Darwin and use echo ID to track flows
|
||||
- 2022-08-18 TUN-6530: Implement ICMPv4 proxy
|
||||
- 2022-08-17 TUN-6666: Define packet package
|
||||
- 2022-08-17 TUN-6667: DatagramMuxerV2 provides a method to receive RawPacket
|
||||
- 2022-08-16 TUN-6657: Ask for Tunnel ID and Configuration on Bug Report
|
||||
- 2022-08-16 TUN-6676: Add suport for trailers in http2 connections
|
||||
- 2022-08-11 TUN-6575: Consume cf-trace-id from incoming http2 TCP requests
|
||||
|
||||
2022.8.2
|
||||
- 2022-08-16 TUN-6656: Docker for arm64 should not be deployed in an amd64 container
|
||||
|
||||
|
||||
@@ -7,15 +7,6 @@ export CGO_ENABLED=0
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
windowsArchs=("amd64" "386")
|
||||
export TARGET_OS=windows
|
||||
for arch in ${windowsArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-msi
|
||||
mv ./cloudflared.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe
|
||||
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
|
||||
done
|
||||
|
||||
|
||||
linuxArchs=("386" "amd64" "arm" "armhf" "arm64")
|
||||
export TARGET_OS=linux
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
//Package carrier provides a WebSocket proxy to carry or proxy a connection
|
||||
//from the local client to the edge. See it as a wrapper around any protocol
|
||||
//that it packages up in a WebSocket connection to the edge.
|
||||
// Package carrier provides a WebSocket proxy to carry or proxy a connection
|
||||
// from the local client to the edge. See it as a wrapper around any protocol
|
||||
// that it packages up in a WebSocket connection to the edge.
|
||||
package carrier
|
||||
|
||||
import (
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/stream"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
@@ -37,7 +38,7 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
cfwebsocket.Stream(wsConn, conn, ws.log)
|
||||
stream.Pipe(wsConn, conn, ws.log)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type namedTunnelToken struct {
|
||||
ZoneID string `json:"zoneID"`
|
||||
AccountID string `json:"accountID"`
|
||||
ServiceKey string `json:"serviceKey"`
|
||||
}
|
||||
|
||||
type OriginCert struct {
|
||||
PrivateKey interface{}
|
||||
Cert *x509.Certificate
|
||||
ZoneID string
|
||||
ServiceKey string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
|
||||
if len(blocks) == 0 {
|
||||
return nil, fmt.Errorf("Cannot decode empty certificate")
|
||||
}
|
||||
originCert := OriginCert{}
|
||||
block, rest := pem.Decode(blocks)
|
||||
for {
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
switch block.Type {
|
||||
case "PRIVATE KEY":
|
||||
if originCert.PrivateKey != nil {
|
||||
return nil, fmt.Errorf("Found multiple private key in the certificate")
|
||||
}
|
||||
// RSA private key
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse private key")
|
||||
}
|
||||
originCert.PrivateKey = privateKey
|
||||
case "CERTIFICATE":
|
||||
if originCert.Cert != nil {
|
||||
return nil, fmt.Errorf("Found multiple certificates in the certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificates(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot parse certificate")
|
||||
} else if len(cert) > 1 {
|
||||
return nil, fmt.Errorf("Found multiple certificates in the certificate")
|
||||
}
|
||||
originCert.Cert = cert[0]
|
||||
case "WARP TOKEN", "ARGO TUNNEL TOKEN":
|
||||
if originCert.ZoneID != "" || originCert.ServiceKey != "" {
|
||||
return nil, fmt.Errorf("Found multiple tokens in the certificate")
|
||||
}
|
||||
// The token is a string,
|
||||
// Try the newer JSON format
|
||||
ntt := namedTunnelToken{}
|
||||
if err := json.Unmarshal(block.Bytes, &ntt); err == nil {
|
||||
originCert.ZoneID = ntt.ZoneID
|
||||
originCert.ServiceKey = ntt.ServiceKey
|
||||
originCert.AccountID = ntt.AccountID
|
||||
} else {
|
||||
// Try the older format, where the zoneID and service key are separated by
|
||||
// a new line character
|
||||
token := string(block.Bytes)
|
||||
s := strings.Split(token, "\n")
|
||||
if len(s) != 2 {
|
||||
return nil, fmt.Errorf("Cannot parse token")
|
||||
}
|
||||
originCert.ZoneID = s[0]
|
||||
originCert.ServiceKey = s[1]
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown block %s in the certificate", block.Type)
|
||||
}
|
||||
block, rest = pem.Decode(rest)
|
||||
}
|
||||
|
||||
if originCert.PrivateKey == nil {
|
||||
return nil, fmt.Errorf("Missing private key in the certificate")
|
||||
} else if originCert.Cert == nil {
|
||||
return nil, fmt.Errorf("Missing certificate in the certificate")
|
||||
} else if originCert.ZoneID == "" || originCert.ServiceKey == "" {
|
||||
return nil, fmt.Errorf("Missing token in the certificate")
|
||||
}
|
||||
|
||||
return &originCert, nil
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package certutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadOriginCert(t *testing.T) {
|
||||
cert, err := DecodeOriginCert([]byte{})
|
||||
assert.Equal(t, fmt.Errorf("Cannot decode empty certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err := ioutil.ReadFile("test-cert-no-key.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Missing private key in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert-two-certificates.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Found multiple certificates in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert-unknown-block.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Unknown block RSA PRIVATE KEY in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err = ioutil.ReadFile("test-cert.pem")
|
||||
assert.Nil(t, err)
|
||||
cert, err = DecodeOriginCert(blocks)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, cert)
|
||||
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
|
||||
key := "v1.0-58bd4f9e28f7b3c28e05a35ff3e80ab4fd9644ef3fece537eb0d12e2e9258217-183442fbb0bbdb3e571558fec9b5589ebd77aafc87498ee3f09f64a4ad79ffe8791edbae08b36c1d8f1d70a8670de56922dff92b15d214a524f4ebfa1958859e-7ce80f79921312a6022c5d25e2d380f82ceaefe3fbdc43dd13b080e3ef1e26f7"
|
||||
assert.Equal(t, key, cert.ServiceKey)
|
||||
}
|
||||
|
||||
func TestNewlineArgoTunnelToken(t *testing.T) {
|
||||
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert.pem")
|
||||
}
|
||||
|
||||
func TestJSONArgoTunnelToken(t *testing.T) {
|
||||
// The given cert's Argo Tunnel Token was generated by base64 encoding this JSON:
|
||||
// {
|
||||
// "zoneID": "7b0a4d77dfb881c1a3b7d61ea9443e19",
|
||||
// "serviceKey": "test-service-key",
|
||||
// "accountID": "abcdabcdabcdabcd1234567890abcdef"
|
||||
// }
|
||||
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert-json.pem")
|
||||
}
|
||||
|
||||
func ArgoTunnelTokenTest(t *testing.T, path string) {
|
||||
blocks, err := ioutil.ReadFile(path)
|
||||
assert.Nil(t, err)
|
||||
cert, err := DecodeOriginCert(blocks)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, cert)
|
||||
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
|
||||
key := "test-service-key"
|
||||
assert.Equal(t, key, cert.ServiceKey)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -1,85 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -1,61 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
|
||||
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
|
||||
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
|
||||
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
|
||||
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
|
||||
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
|
||||
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
|
||||
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
|
||||
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
|
||||
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
|
||||
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
|
||||
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
|
||||
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
|
||||
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
|
||||
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
|
||||
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
|
||||
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
|
||||
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
|
||||
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
|
||||
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
|
||||
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
|
||||
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
|
||||
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
|
||||
uQicoQq3yzeQh20wtrtaXzTNmA==
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
|
||||
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
|
||||
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
|
||||
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
|
||||
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
|
||||
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
|
||||
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
|
||||
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
|
||||
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
|
||||
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
|
||||
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
|
||||
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
|
||||
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
|
||||
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
|
||||
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
|
||||
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
|
||||
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
|
||||
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
|
||||
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
|
||||
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
@@ -104,7 +104,7 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (
|
||||
if bodyReader != nil {
|
||||
req.Header.Set("Content-Type", jsonContentType)
|
||||
}
|
||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.authToken))
|
||||
req.Header.Add("Accept", "application/json;version=1")
|
||||
return r.client.Do(req)
|
||||
}
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ type TunnelClient interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
GetTunnelToken(tunnelID uuid.UUID) (string, error)
|
||||
GetManagementToken(tunnelID uuid.UUID) (string, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
@@ -28,7 +29,7 @@ type IPRouteClient interface {
|
||||
type VnetClient interface {
|
||||
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
|
||||
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
|
||||
DeleteVirtualNetwork(id uuid.UUID) error
|
||||
DeleteVirtualNetwork(id uuid.UUID, force bool) error
|
||||
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ type newTunnel struct {
|
||||
TunnelSecret []byte `json:"tunnel_secret"`
|
||||
}
|
||||
|
||||
type managementRequest struct {
|
||||
Resources []string `json:"resources"`
|
||||
}
|
||||
|
||||
type CleanupParams struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
@@ -133,6 +137,28 @@ func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error
|
||||
return "", r.statusCodeToError("get tunnel token", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID))
|
||||
|
||||
body := &managementRequest{
|
||||
Resources: []string{"logs"},
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", endpoint, body)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
err = parseResponse(resp.Body, &token)
|
||||
return token, err
|
||||
}
|
||||
|
||||
return "", r.statusCodeToError("get tunnel token", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
|
||||
@@ -80,9 +80,16 @@ func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork,
|
||||
return nil, r.statusCodeToError("list virtual networks", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
|
||||
func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID, force bool) error {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
|
||||
queryParams := url.Values{}
|
||||
if force {
|
||||
queryParams.Set("force", strconv.FormatBool(force))
|
||||
}
|
||||
endpoint.RawQuery = queryParams.Encode()
|
||||
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
|
||||
+67
-58
@@ -1,23 +1,29 @@
|
||||
pinned_go: &pinned_go go=1.18.5-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.18.5-2
|
||||
pinned_go: &pinned_go go=1.19.6-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.19.6-1
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: bullseye
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: &build_deps
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
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_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: &build_deps_fips
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
@@ -78,6 +84,13 @@ stretch: &stretch
|
||||
# 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:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- make generate-docker-version
|
||||
build-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deb_deps
|
||||
@@ -119,21 +132,9 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
publish-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- openssh-client
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-deb
|
||||
github-release-macos-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: &build_pygithub
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
@@ -145,14 +146,31 @@ stretch: &stretch
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
- make github-mac-upload
|
||||
test:
|
||||
github-release-windows:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: &test_pre_cache
|
||||
- go install golang.org/x/tools/cmd/goimports@latest
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
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:
|
||||
- .teamcity/package-windows.sh
|
||||
- make github-windows-upload
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deps
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
@@ -161,11 +179,8 @@ stretch: &stretch
|
||||
- make test | gotest-to-teamcity
|
||||
test-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: *test_pre_cache
|
||||
builddeps: *build_deps_fips
|
||||
pre-cache: *build_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
@@ -174,6 +189,26 @@ stretch: &stretch
|
||||
- ./fmt-check.sh
|
||||
- make test | gotest-to-teamcity
|
||||
component-test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- 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
|
||||
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 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
|
||||
@@ -185,32 +220,21 @@ stretch: &stretch
|
||||
- procps
|
||||
pre-cache-copy-paths:
|
||||
- component-tests/requirements.txt
|
||||
pre-cache:
|
||||
- sudo pip3 install --upgrade -r component-tests/requirements.txt
|
||||
post-cache:
|
||||
# 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
|
||||
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:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
builddeps: *build_pygithub
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- make github-message
|
||||
@@ -221,13 +245,13 @@ stretch: &stretch
|
||||
- build-essential
|
||||
- python3
|
||||
- genisoimage
|
||||
- jetez
|
||||
pre-cache:
|
||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
||||
post-cache:
|
||||
- export CGO_ENABLED=0
|
||||
- export GOOS=freebsd
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-junos
|
||||
- make cloudflared
|
||||
publish-junos:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -247,18 +271,3 @@ stretch: &stretch
|
||||
buster: *stretch
|
||||
bullseye: *stretch
|
||||
bookworm: *stretch
|
||||
centos-7:
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: &el7_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://go.dev/dl/go1.18.5.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.18.5.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
|
||||
+39
-37
@@ -1,62 +1,64 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<?if $(var.Platform)="x86"?>
|
||||
<?define Program_Files="ProgramFilesFolder"?>
|
||||
<?else?>
|
||||
<?if $(var.Platform)="x64" ?>
|
||||
<?define Program_Files="ProgramFiles64Folder"?>
|
||||
<?endif?>
|
||||
<?else ?>
|
||||
<?define Program_Files="ProgramFilesFolder"?>
|
||||
<?endif ?>
|
||||
<?ifndef var.Version?>
|
||||
<?error Undefined Version variable?>
|
||||
<?endif?>
|
||||
<?endif ?>
|
||||
<?ifndef var.Path?>
|
||||
<?error Undefined Path variable?>
|
||||
<?endif?>
|
||||
<?endif ?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="35e5e858-9372-4449-bf73-1cd6f7267128"
|
||||
<Product Id="*"
|
||||
UpgradeCode="23f90fdd-9328-47ea-ab52-5380855a4b12"
|
||||
Name="cloudflared"
|
||||
Version="$(var.Version)"
|
||||
Manufacturer="cloudflare"
|
||||
Language="1033">
|
||||
|
||||
<Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package" InstallScope="perMachine"/>
|
||||
<Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package" InstallScope="perMachine" />
|
||||
|
||||
<Media Id="1" Cabinet="product.cab" EmbedCab="yes"/>
|
||||
<Media Id="1" Cabinet="product.cab" EmbedCab="yes" />
|
||||
|
||||
<Upgrade Id="23f90fdd-9328-47ea-ab52-5380855a4b12">
|
||||
<UpgradeVersion Minimum="$(var.Version)" OnlyDetect="yes" Property="NEWERVERSIONDETECTED"/>
|
||||
<UpgradeVersion Minimum="2020.8.0" Maximum="$(var.Version)" IncludeMinimum="yes" IncludeMaximum="no"
|
||||
Property="OLDERVERSIONBEINGUPGRADED"/>
|
||||
</Upgrade>
|
||||
<Condition Message="A newer version of this software is already installed.">NOT NEWERVERSIONDETECTED</Condition>
|
||||
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<!--This specifies where the cloudflared.exe is moved to in the windows Operation System-->
|
||||
<Directory Id="$(var.Program_Files)">
|
||||
<Directory Id="INSTALLDIR" Name="cloudflared">
|
||||
<Component Id="ApplicationFiles" Guid="35e5e858-9372-4449-bf73-1cd6f7267128">
|
||||
<File Id="ApplicationFile0" Source="$(var.Path)"/>
|
||||
</Component>
|
||||
<Upgrade Id="23f90fdd-9328-47ea-ab52-5380855a4b12">
|
||||
<UpgradeVersion Minimum="$(var.Version)" OnlyDetect="yes" Property="NEWERVERSIONDETECTED" />
|
||||
<UpgradeVersion Minimum="2020.8.0" Maximum="$(var.Version)" IncludeMinimum="yes" IncludeMaximum="no"
|
||||
Property="OLDERVERSIONBEINGUPGRADED" />
|
||||
</Upgrade>
|
||||
<Condition Message="A newer version of this software is already installed.">NOT NEWERVERSIONDETECTED</Condition>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<!--This specifies where the cloudflared.exe is moved to in the windows Operation System-->
|
||||
<Directory Id="$(var.Program_Files)">
|
||||
<Directory Id="INSTALLDIR" Name="cloudflared">
|
||||
<Component Id="ApplicationFiles" Guid="35e5e858-9372-4449-bf73-1cd6f7267128">
|
||||
<File Id="ApplicationFile0" Source="$(var.Path)" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<Component Id="ENVS" Guid="6bb74449-d10d-4f4a-933e-6fc9fa006eae">
|
||||
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
||||
<Environment Id="ENV0"
|
||||
Name="PATH"
|
||||
Value="[INSTALLDIR]."
|
||||
Permanent="no"
|
||||
Part="last"
|
||||
Action="create"
|
||||
System="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
<Component Id="ENVS" Guid="6bb74449-d10d-4f4a-933e-6fc9fa006eae">
|
||||
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
||||
<Environment Id="ENV0"
|
||||
Name="PATH"
|
||||
Value="[INSTALLDIR]."
|
||||
Permanent="no"
|
||||
Part="last"
|
||||
Action="create"
|
||||
System="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentRef Id="ENVS"/>
|
||||
<ComponentRef Id='ApplicationFiles' />
|
||||
</Feature>
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentRef Id="ENVS" />
|
||||
<ComponentRef Id='ApplicationFiles' />
|
||||
</Feature>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -37,16 +37,13 @@ const (
|
||||
sshConfigTemplate = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
Host {{.Hostname}}
|
||||
{{- if .ShortLivedCerts}}
|
||||
ProxyCommand bash -c '{{.Cloudflared}} access ssh-gen --hostname %h; ssh -tt %r@cfpipe-{{.Hostname}} >&2 <&1'
|
||||
|
||||
Host cfpipe-{{.Hostname}}
|
||||
HostName {{.Hostname}}
|
||||
Match host {{.Hostname}} exec "{{.Cloudflared}} access ssh-gen --hostname %h"
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
IdentityFile ~/.cloudflared/{{.Hostname}}-cf_key
|
||||
CertificateFile ~/.cloudflared/{{.Hostname}}-cf_key-cert.pub
|
||||
IdentityFile ~/.cloudflared/%h-cf_key
|
||||
CertificateFile ~/.cloudflared/%h-cf_key-cert.pub
|
||||
{{- else}}
|
||||
Host {{.Hostname}}
|
||||
ProxyCommand {{.Cloudflared}} access ssh --hostname %h
|
||||
{{end}}
|
||||
`
|
||||
@@ -205,7 +202,11 @@ func Commands() []*cli.Command {
|
||||
|
||||
// login pops up the browser window to do the actual login and JWT generation
|
||||
func login(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryDSN,
|
||||
Release: c.App.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -254,7 +255,11 @@ func ensureURLScheme(url string) string {
|
||||
|
||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||
func curl(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryDSN,
|
||||
Release: c.App.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
@@ -275,6 +280,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 {
|
||||
@@ -317,7 +329,11 @@ func run(cmd string, args ...string) error {
|
||||
|
||||
// token dumps provided token to stdout
|
||||
func generateToken(c *cli.Context) error {
|
||||
if err := raven.SetDSN(sentryDSN); err != nil {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryDSN,
|
||||
Release: c.App.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
|
||||
|
||||
@@ -47,3 +47,7 @@ func (bi *BuildInfo) GetBuildTypeMsg() string {
|
||||
}
|
||||
return fmt.Sprintf(" with %s", bi.BuildType)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) UserAgent() string {
|
||||
return fmt.Sprintf("cloudflared/%s", bi.CloudflaredVersion)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
debugLevelWarning = "At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. " +
|
||||
"This can expose sensitive information in your logs."
|
||||
)
|
||||
|
||||
func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogLevelFlag,
|
||||
Value: "info",
|
||||
Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning,
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogTransportLevelFlag,
|
||||
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
|
||||
Value: "info",
|
||||
Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}",
|
||||
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogFileFlag,
|
||||
Usage: "Save application log to this file for reporting issues.",
|
||||
EnvVars: []string{"TUNNEL_LOGFILE"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogDirectoryFlag,
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "trace-output",
|
||||
Usage: "Name of trace output file, generated when cloudflared stops.",
|
||||
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tail"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
@@ -50,7 +51,6 @@ var (
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
maxprocs.Set()
|
||||
bInfo := cliutil.GetBuildInfo(BuildType, Version)
|
||||
|
||||
@@ -90,6 +90,7 @@ func main() {
|
||||
updater.Init(Version)
|
||||
tracing.Init(Version)
|
||||
token.Init(Version)
|
||||
tail.Init(bInfo)
|
||||
runApp(app, graceShutdownC)
|
||||
}
|
||||
|
||||
@@ -139,6 +140,7 @@ To determine if an update happened in a script, check for error code 11.`,
|
||||
cmds = append(cmds, tunnel.Commands()...)
|
||||
cmds = append(cmds, proxydns.Command(false))
|
||||
cmds = append(cmds, access.Commands()...)
|
||||
cmds = append(cmds, tail.Command())
|
||||
return cmds
|
||||
}
|
||||
|
||||
@@ -156,10 +158,10 @@ func action(graceShutdownC chan struct{}) cli.ActionFunc {
|
||||
if isEmptyInvocation(c) {
|
||||
return handleServiceMode(c, graceShutdownC)
|
||||
}
|
||||
tags := make(map[string]string)
|
||||
tags["hostname"] = c.String("hostname")
|
||||
raven.SetTagsContext(tags)
|
||||
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
|
||||
func() {
|
||||
defer sentry.Recover()
|
||||
err = tunnel.TunnelCommand(c)
|
||||
}()
|
||||
if err != nil {
|
||||
captureError(err)
|
||||
}
|
||||
@@ -187,7 +189,7 @@ func captureError(err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
raven.CaptureError(err, nil)
|
||||
sentry.CaptureException(err)
|
||||
}
|
||||
|
||||
// cloudflared was started without any flags
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package proxydns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -73,7 +74,7 @@ func Run(c *cli.Context) error {
|
||||
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
|
||||
}
|
||||
|
||||
go metrics.ServeMetrics(metricsListener, nil, nil, "", nil, log)
|
||||
go metrics.ServeMetrics(metricsListener, context.Background(), metrics.Config{}, log)
|
||||
|
||||
listener, err := tunneldns.CreateListener(
|
||||
c.String("address"),
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
package tail
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"nhooyr.io/websocket"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
)
|
||||
|
||||
var (
|
||||
buildInfo *cliutil.BuildInfo
|
||||
)
|
||||
|
||||
func Init(bi *cliutil.BuildInfo) {
|
||||
buildInfo = bi
|
||||
}
|
||||
|
||||
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,
|
||||
Usage: "Stream logs from a remote cloudflared",
|
||||
UsageText: "cloudflared tail [tail command options] [TUNNEL-ID]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "connector-id",
|
||||
Usage: "Access a specific cloudflared instance by connector id (for when a tunnel has multiple cloudflared's)",
|
||||
Value: "",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_CONNECTOR"},
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "event",
|
||||
Usage: "Filter by specific Events (cloudflared, http, tcp, udp) otherwise, defaults to send all events",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_FILTER_EVENTS"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "level",
|
||||
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: "management-hostname",
|
||||
Usage: "Management hostname to signify incoming management requests",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_HOSTNAME"},
|
||||
Hidden: true,
|
||||
Value: "management.argotunnel.com",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "trace",
|
||||
Usage: "Set a cf-trace-id for the request",
|
||||
Hidden: true,
|
||||
Value: "",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogLevelFlag,
|
||||
Value: "info",
|
||||
Usage: "Application logging level {debug, info, warn, error, fatal}",
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: credentials.OriginCertFlag,
|
||||
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
|
||||
Value: credentials.FindDefaultOriginCertPath(),
|
||||
},
|
||||
},
|
||||
Subcommands: subcommands,
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware validation error struct for returning to the eyeball
|
||||
type managementError struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Middleware validation error HTTP response JSON for returning to the eyeball
|
||||
type managementErrorResponse struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []managementError `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func handleValidationError(resp *http.Response, log *zerolog.Logger) {
|
||||
if resp.StatusCode == 530 {
|
||||
log.Error().Msgf("no cloudflared connector available or reachable via management request (a recent version of cloudflared is required to use streaming logs)")
|
||||
}
|
||||
var managementErr managementErrorResponse
|
||||
err := json.NewDecoder(resp.Body).Decode(&managementErr)
|
||||
if err != nil {
|
||||
log.Error().Msgf("unable to start management log streaming session: http response code returned %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
if managementErr.Success || len(managementErr.Errors) == 0 {
|
||||
log.Error().Msgf("management tunnel validation returned success with invalid HTTP response code to convert to a WebSocket request")
|
||||
return
|
||||
}
|
||||
for _, e := range managementErr.Errors {
|
||||
log.Error().Msgf("management request failed validation: (%d) %s", e.Code, e.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// logger will be created to emit only against the os.Stderr as to not obstruct with normal output from
|
||||
// management requests
|
||||
func createLogger(c *cli.Context) *zerolog.Logger {
|
||||
level, levelErr := zerolog.ParseLevel(c.String(logger.LogLevelFlag))
|
||||
if levelErr != nil {
|
||||
level = zerolog.InfoLevel
|
||||
}
|
||||
log := zerolog.New(zerolog.ConsoleWriter{
|
||||
Out: colorable.NewColorable(os.Stderr),
|
||||
TimeFormat: time.RFC3339,
|
||||
}).With().Timestamp().Logger().Level(level)
|
||||
return &log
|
||||
}
|
||||
|
||||
// parseFilters will attempt to parse provided filters to send to with the EventStartStreaming
|
||||
func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
|
||||
var level *management.LogLevel
|
||||
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)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid --level filter provided, please use one of the following Log Levels: debug, info, warn, error")
|
||||
}
|
||||
level = &l
|
||||
}
|
||||
|
||||
for _, v := range argEvents {
|
||||
t, ok := management.ParseLogEventType(v)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid --event filter provided, please use one of the following EventTypes: cloudflared, http, tcp, udp")
|
||||
}
|
||||
events = append(events, t)
|
||||
}
|
||||
|
||||
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,
|
||||
Sampling: sample,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getManagementToken will make a call to the Cloudflare API to acquire a management token for the requested tunnel.
|
||||
func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
|
||||
userCreds, err := credentials.Read(c.String(credentials.OriginCertFlag), log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client, err := userCreds.Client(c.String("api-url"), buildInfo.UserAgent(), log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tunnelIDString := c.Args().First()
|
||||
if tunnelIDString == "" {
|
||||
return "", errors.New("no tunnel ID provided")
|
||||
}
|
||||
tunnelID, err := uuid.Parse(tunnelIDString)
|
||||
if err != nil {
|
||||
return "", errors.New("unable to parse provided tunnel id as a valid UUID")
|
||||
}
|
||||
|
||||
token, err := client.GetManagementToken(tunnelID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// buildURL will build the management url to contain the required query parameters to authenticate the request.
|
||||
func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
|
||||
var err error
|
||||
managementHostname := c.String("management-hostname")
|
||||
token := c.String("token")
|
||||
if token == "" {
|
||||
token, err = getManagementToken(c, log)
|
||||
if err != nil {
|
||||
return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err)
|
||||
}
|
||||
}
|
||||
query := url.Values{}
|
||||
query.Add("access_token", token)
|
||||
connector := c.String("connector-id")
|
||||
if connector != "" {
|
||||
connectorID, err := uuid.Parse(connector)
|
||||
if err != nil {
|
||||
return url.URL{}, fmt.Errorf("unabled to parse 'connector-id' flag into a valid UUID: %w", err)
|
||||
}
|
||||
query.Add("connector_id", connectorID.String())
|
||||
}
|
||||
return url.URL{Scheme: "wss", Host: managementHostname, Path: "/logs", RawQuery: query.Encode()}, nil
|
||||
}
|
||||
|
||||
// Run implements a foreground runner
|
||||
func Run(c *cli.Context) error {
|
||||
log := createLogger(c)
|
||||
|
||||
signals := make(chan os.Signal, 10)
|
||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||
defer signal.Stop(signals)
|
||||
|
||||
filters, err := parseFilters(c)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("invalid filters provided")
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := buildURL(c, log)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("unable to construct management request URL")
|
||||
return nil
|
||||
}
|
||||
|
||||
header := make(http.Header)
|
||||
header.Add("User-Agent", buildInfo.UserAgent())
|
||||
trace := c.String("trace")
|
||||
if trace != "" {
|
||||
header["cf-trace-id"] = []string{trace}
|
||||
}
|
||||
ctx := c.Context
|
||||
conn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
|
||||
HTTPHeader: header,
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
handleValidationError(resp, log)
|
||||
return nil
|
||||
}
|
||||
log.Error().Err(err).Msgf("unable to start management log streaming session")
|
||||
return nil
|
||||
}
|
||||
defer conn.Close(websocket.StatusInternalError, "management connection was closed abruptly")
|
||||
|
||||
// Once connection is established, send start_streaming event to begin receiving logs
|
||||
err = management.WriteEvent(conn, ctx, &management.EventStartStreaming{
|
||||
ClientEvent: management.ClientEvent{Type: management.StartStreaming},
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to request logs from management tunnel")
|
||||
return nil
|
||||
}
|
||||
log.Debug().
|
||||
Str("tunnel-id", c.Args().First()).
|
||||
Str("connector-id", c.String("connector-id")).
|
||||
Interface("filters", filters).
|
||||
Msg("connected")
|
||||
|
||||
readerDone := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(readerDone)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
event, err := management.ReadServerEvent(conn, ctx)
|
||||
if err != nil {
|
||||
if closeErr := management.AsClosed(err); closeErr != nil {
|
||||
// If the client (or the server) already closed the connection, don't continue to
|
||||
// attempt to read from the client.
|
||||
if closeErr.Code == websocket.StatusNormalClosure {
|
||||
return
|
||||
}
|
||||
// Only log abnormal closures
|
||||
log.Error().Msgf("received remote closure: (%d) %s", closeErr.Code, closeErr.Reason)
|
||||
return
|
||||
}
|
||||
log.Err(err).Msg("unable to read event from server")
|
||||
return
|
||||
}
|
||||
switch event.Type {
|
||||
case management.Logs:
|
||||
logs, ok := management.IntoServerEvent(event, management.Logs)
|
||||
if !ok {
|
||||
log.Error().Msgf("invalid logs event")
|
||||
continue
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
fmt.Printf("%s %s %s %s %s\n", l.Time, l.Level, l.Event, l.Message, fields)
|
||||
}
|
||||
case management.UnknownServerEventType:
|
||||
fallthrough
|
||||
default:
|
||||
log.Debug().Msgf("unexpected log event type: %s", event.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-readerDone:
|
||||
return nil
|
||||
case <-signals:
|
||||
log.Debug().Msg("closing management connection")
|
||||
// Cleanly close the connection by sending a close message and then
|
||||
// waiting (with timeout) for the server to close the connection.
|
||||
conn.Close(websocket.StatusNormalClosure, "")
|
||||
select {
|
||||
case <-readerDone:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
-74
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/google/uuid"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
@@ -28,19 +28,27 @@ import (
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/validation"
|
||||
)
|
||||
|
||||
const (
|
||||
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
|
||||
|
||||
// ha-Connections specifies how many connections to make to the edge
|
||||
haConnectionsFlag = "ha-connections"
|
||||
|
||||
// sshPortFlag is the port on localhost the cloudflared ssh server will run on
|
||||
sshPortFlag = "local-ssh-port"
|
||||
|
||||
@@ -74,14 +82,21 @@ const (
|
||||
// uiFlag is to enable launching cloudflared in interactive UI mode
|
||||
uiFlag = "ui"
|
||||
|
||||
debugLevelWarning = "At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. " +
|
||||
"This can expose sensitive information in your logs."
|
||||
|
||||
LogFieldCommand = "command"
|
||||
LogFieldExpandedPath = "expandedPath"
|
||||
LogFieldPIDPathname = "pidPathname"
|
||||
LogFieldTmpTraceFilename = "tmpTraceFilename"
|
||||
LogFieldTraceOutputFilepath = "traceOutputFilepath"
|
||||
|
||||
tunnelCmdErrorMessage = `You did not specify any valid additional argument to the cloudflared tunnel command.
|
||||
|
||||
If you are trying to run a Quick Tunnel then you need to explicitly pass the --url flag.
|
||||
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 (
|
||||
@@ -91,6 +106,7 @@ var (
|
||||
routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+
|
||||
"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/)")
|
||||
)
|
||||
|
||||
func Flags() []cli.Flag {
|
||||
@@ -166,21 +182,54 @@ func TunnelCommand(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if name := c.String("name"); name != "" { // Start a named tunnel
|
||||
|
||||
// Run a adhoc named tunnel
|
||||
// Allows for the creation, routing (optional), and startup of a tunnel in one command
|
||||
// --name required
|
||||
// --url or --hello-world required
|
||||
// --hostname optional
|
||||
if name := c.String("name"); name != "" {
|
||||
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid hostname provided")
|
||||
}
|
||||
url := c.String("url")
|
||||
if url == hostname && url != "" && hostname != "" {
|
||||
return fmt.Errorf("hostname and url shouldn't match. See --help for more information")
|
||||
}
|
||||
|
||||
return runAdhocNamedTunnel(sc, name, c.String(CredFileFlag))
|
||||
}
|
||||
|
||||
// Run a quick tunnel
|
||||
// A unauthenticated named tunnel hosted on <random>.<quick-tunnels-service>.com
|
||||
// We don't support running proxy-dns and a quick tunnel at the same time as the same process
|
||||
shouldRunQuickTunnel := c.IsSet("url") || c.IsSet(ingress.HelloWorldFlag)
|
||||
if !c.IsSet("proxy-dns") && c.String("quick-service") != "" && shouldRunQuickTunnel {
|
||||
return RunQuickTunnel(sc)
|
||||
}
|
||||
|
||||
// If user provides a config, check to see if they meant to use `tunnel run` instead
|
||||
if ref := config.GetConfiguration().TunnelID; ref != "" {
|
||||
return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
|
||||
}
|
||||
|
||||
// Unauthenticated named tunnel on <random>.<quick-tunnels-service>.com
|
||||
// For now, default to legacy setup unless quick-service is specified
|
||||
if !dnsProxyStandAlone(c, nil) && c.String("hostname") == "" && c.String("quick-service") != "" {
|
||||
return RunQuickTunnel(sc)
|
||||
// Classic tunnel usage is no longer supported
|
||||
if c.String("hostname") != "" {
|
||||
return deprecatedClassicTunnelErr
|
||||
}
|
||||
|
||||
// Start a classic tunnel
|
||||
return runClassicTunnel(sc)
|
||||
if c.IsSet("proxy-dns") {
|
||||
if shouldRunQuickTunnel {
|
||||
return fmt.Errorf("running a quick tunnel with `proxy-dns` is not supported")
|
||||
}
|
||||
// NamedTunnelProperties are nil since proxy dns server does not need it.
|
||||
// This is supported for legacy reasons: dns proxy server is not a tunnel and ideally should
|
||||
// not run as part of cloudflared tunnel.
|
||||
return StartServer(sc.c, buildInfo, nil, sc.log)
|
||||
}
|
||||
|
||||
return errors.New(tunnelCmdErrorMessage)
|
||||
}
|
||||
|
||||
func Init(info *cliutil.BuildInfo, gracefulShutdown chan struct{}) {
|
||||
@@ -215,11 +264,6 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// runClassicTunnel creates a "classic" non-named tunnel
|
||||
func runClassicTunnel(sc *subcommandContext) error {
|
||||
return StartServer(sc.c, buildInfo, nil, sc.log)
|
||||
}
|
||||
|
||||
func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
|
||||
if hostname := c.String("hostname"); hostname != "" {
|
||||
if lbPool := c.String("lb-pool"); lbPool != "" {
|
||||
@@ -236,12 +280,19 @@ func StartServer(
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
log *zerolog.Logger,
|
||||
) error {
|
||||
_ = raven.SetDSN(sentryDSN)
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryDSN,
|
||||
Release: c.App.Version,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
listeners := gracenet.Net{}
|
||||
errC := make(chan error)
|
||||
|
||||
if config.GetConfiguration().Source() == "" {
|
||||
// Only log for locally configured tunnels (Token is blank).
|
||||
if config.GetConfiguration().Source() == "" && c.String(TunnelTokenFlag) == "" {
|
||||
log.Info().Msg(config.ErrNoConfigFile.Error())
|
||||
}
|
||||
|
||||
@@ -314,21 +365,13 @@ func StartServer(
|
||||
errC <- autoupdater.Run(ctx)
|
||||
}()
|
||||
|
||||
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
|
||||
// Serve DNS proxy stand-alone if no tunnel type (quick, adhoc, named) is going to run
|
||||
if dnsProxyStandAlone(c, namedTunnel) {
|
||||
connectedSignal.Notify()
|
||||
// no grace period, handle SIGINT/SIGTERM immediately
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, log)
|
||||
}
|
||||
|
||||
url := c.String("url")
|
||||
hostname := c.String("hostname")
|
||||
if url == hostname && url != "" && hostname != "" {
|
||||
errText := "hostname and url shouldn't match. See --help for more information"
|
||||
log.Error().Msg(errText)
|
||||
return fmt.Errorf(errText)
|
||||
}
|
||||
|
||||
logTransport := logger.CreateTransportLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
observer := connection.NewObserver(log, logTransport)
|
||||
@@ -356,7 +399,26 @@ func StartServer(
|
||||
}
|
||||
}
|
||||
|
||||
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, tunnelConfig.Log)
|
||||
localRules := []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"),
|
||||
serviceIP,
|
||||
clientID,
|
||||
c.String(connectorLabelFlag),
|
||||
logger.ManagementLogger.Log,
|
||||
logger.ManagementLogger,
|
||||
)
|
||||
localRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
|
||||
}
|
||||
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, localRules, tunnelConfig.Log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -372,10 +434,15 @@ func StartServer(
|
||||
defer wg.Done()
|
||||
readinessServer := metrics.NewReadyServer(log, clientID)
|
||||
observer.RegisterSink(readinessServer)
|
||||
errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, orchestrator, log)
|
||||
metricsConfig := metrics.Config{
|
||||
ReadyServer: readinessServer,
|
||||
QuickTunnelHostname: quickTunnelURL,
|
||||
Orchestrator: orchestrator,
|
||||
}
|
||||
errC <- metrics.ServeMetrics(metricsListener, ctx, metricsConfig, log)
|
||||
}()
|
||||
|
||||
reconnectCh := make(chan supervisor.ReconnectSignal, 1)
|
||||
reconnectCh := make(chan supervisor.ReconnectSignal, c.Int(haConnectionsFlag))
|
||||
if c.IsSet("stdin-control") {
|
||||
log.Info().Msg("Enabling control through stdin")
|
||||
go stdinControl(reconnectCh, log)
|
||||
@@ -488,7 +555,7 @@ func addPortIfMissing(uri *url.URL, port int) string {
|
||||
func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
flags := configureCloudflaredFlags(shouldHide)
|
||||
flags = append(flags, configureProxyFlags(shouldHide)...)
|
||||
flags = append(flags, configureLoggingFlags(shouldHide)...)
|
||||
flags = append(flags, cliutil.ConfigureLoggingFlags(shouldHide)...)
|
||||
flags = append(flags, configureProxyDNSFlags(shouldHide)...)
|
||||
flags = append(flags, []cli.Flag{
|
||||
credentialsFileFlag,
|
||||
@@ -511,11 +578,17 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "edge-ip-version",
|
||||
Usage: "Cloudflare Edge ip address version to connect with. {4, 6, auto}",
|
||||
Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}",
|
||||
EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"},
|
||||
Value: "4",
|
||||
Hidden: false,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "edge-bind-address",
|
||||
Usage: "Bind to IP address for outgoing connections to Cloudflare Edge.",
|
||||
EnvVars: []string{"TUNNEL_EDGE_BIND_ADDRESS"},
|
||||
Hidden: false,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: tlsconfig.CaCertFlag,
|
||||
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
|
||||
@@ -591,6 +664,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Value: 5,
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "max-edge-addr-retries",
|
||||
Usage: "Maximum number of times to retry on edge addrs before falling back to a lower protocol",
|
||||
Value: 8,
|
||||
Hidden: true,
|
||||
}),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "retries",
|
||||
@@ -600,10 +679,15 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "ha-connections",
|
||||
Name: haConnectionsFlag,
|
||||
Value: 4,
|
||||
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.",
|
||||
@@ -665,6 +749,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_MAX_FETCH_SIZE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "post-quantum",
|
||||
Usage: "When given creates an experimental post-quantum secure tunnel",
|
||||
Aliases: []string{"pq"},
|
||||
EnvVars: []string{"TUNNEL_POST_QUANTUM"},
|
||||
Hidden: FipsEnabled,
|
||||
}),
|
||||
selectProtocolFlag,
|
||||
overwriteDNSFlag,
|
||||
}...)
|
||||
@@ -682,10 +773,10 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
},
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "origincert",
|
||||
Name: credentials.OriginCertFlag,
|
||||
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
|
||||
Value: findDefaultOriginCertPath(),
|
||||
Value: credentials.FindDefaultOriginCertPath(),
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
@@ -727,7 +818,7 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "hello-world",
|
||||
Name: ingress.HelloWorldFlag,
|
||||
Value: false,
|
||||
Usage: "Run Hello World Server",
|
||||
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
|
||||
@@ -830,6 +921,20 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
Value: false,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "management-hostname",
|
||||
Usage: "Management hostname to signify incoming management requests",
|
||||
EnvVars: []string{"TUNNEL_MANAGEMENT_HOSTNAME"},
|
||||
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)...)
|
||||
}
|
||||
@@ -938,44 +1043,6 @@ func sshFlags(shouldHide bool) []cli.Flag {
|
||||
}
|
||||
}
|
||||
|
||||
func configureLoggingFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogLevelFlag,
|
||||
Value: "info",
|
||||
Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning,
|
||||
EnvVars: []string{"TUNNEL_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogTransportLevelFlag,
|
||||
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
|
||||
Value: "info",
|
||||
Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}",
|
||||
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogFileFlag,
|
||||
Usage: "Save application log to this file for reporting issues.",
|
||||
EnvVars: []string{"TUNNEL_LOGFILE"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: logger.LogDirectoryFlag,
|
||||
Usage: "Save application log to this directory for reporting issues.",
|
||||
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "trace-output",
|
||||
Usage: "Name of trace output file, generated when cloudflared stops.",
|
||||
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
|
||||
@@ -3,14 +3,14 @@ package tunnel
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
mathRand "math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -18,21 +18,18 @@ import (
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"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/validation"
|
||||
)
|
||||
|
||||
const LogFieldOriginCertPath = "originCertPath"
|
||||
const secretValue = "*****"
|
||||
|
||||
var (
|
||||
@@ -40,26 +37,11 @@ var (
|
||||
serviceUrl = developerPortal + "/reference/service/"
|
||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
||||
|
||||
LogFieldHostname = "hostname"
|
||||
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
|
||||
|
||||
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
|
||||
defaultFeatures = []string{supervisor.FeatureAllowRemoteConfig, supervisor.FeatureSerializedHeaders}
|
||||
|
||||
configFlags = []string{"autoupdate-freq", "no-autoupdate", "retries", "protocol", "loglevel", "transport-loglevel", "origincert", "metrics", "metrics-update-freq", "edge-ip-version"}
|
||||
configFlags = []string{"autoupdate-freq", "no-autoupdate", "retries", "protocol", "loglevel", "transport-loglevel", "origincert", "metrics", "metrics-update-freq", "edge-ip-version", "edge-bind-address"}
|
||||
)
|
||||
|
||||
// returns the first path that contains a cert.pem file. If none of the DefaultConfigSearchDirectories
|
||||
// contains a cert.pem file, return empty string
|
||||
func findDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
|
||||
if ok, _ := config.FileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func generateRandomClientID(log *zerolog.Logger) (string, error) {
|
||||
u, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
@@ -124,63 +106,10 @@ func isSecretEnvVar(key string) bool {
|
||||
}
|
||||
|
||||
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelProperties) bool {
|
||||
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world") && namedTunnel == nil)
|
||||
}
|
||||
|
||||
func findOriginCert(originCertPath string, log *zerolog.Logger) (string, error) {
|
||||
if originCertPath == "" {
|
||||
log.Info().Msgf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigSearchDirectories())
|
||||
if isRunningFromTerminal() {
|
||||
log.Error().Msgf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
|
||||
return "", fmt.Errorf("client didn't specify origincert path when running from terminal")
|
||||
} else {
|
||||
log.Error().Msgf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||||
return "", fmt.Errorf("client didn't specify origincert path")
|
||||
}
|
||||
}
|
||||
var err error
|
||||
originCertPath, err = homedir.Expand(originCertPath)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Cannot resolve origin certificate path")
|
||||
return "", fmt.Errorf("cannot resolve path %s", originCertPath)
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
ok, err := config.FileExists(originCertPath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Cannot check if origin cert exists at path %s", originCertPath)
|
||||
return "", fmt.Errorf("cannot check if origin cert exists at path %s", originCertPath)
|
||||
}
|
||||
if !ok {
|
||||
log.Error().Msgf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
If the path above is wrong, specify the path with the -origincert option.
|
||||
If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
%s login
|
||||
`, originCertPath, os.Args[0])
|
||||
return "", fmt.Errorf("cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
|
||||
return originCertPath, nil
|
||||
}
|
||||
|
||||
func readOriginCert(originCertPath string) ([]byte, error) {
|
||||
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||||
originCert, err := ioutil.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
func getOriginCert(originCertPath string, log *zerolog.Logger) ([]byte, error) {
|
||||
if originCertPath, err := findOriginCert(originCertPath, log); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return readOriginCert(originCertPath)
|
||||
}
|
||||
return c.IsSet("proxy-dns") &&
|
||||
!(c.IsSet("name") || // adhoc-named tunnel
|
||||
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
|
||||
namedTunnel != nil) // named tunnel
|
||||
}
|
||||
|
||||
func prepareTunnelConfig(
|
||||
@@ -190,108 +119,48 @@ func prepareTunnelConfig(
|
||||
observer *connection.Observer,
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
) (*supervisor.TunnelConfig, *orchestration.Config, error) {
|
||||
isNamedTunnel := namedTunnel != nil
|
||||
|
||||
configHostname := c.String("hostname")
|
||||
hostname, err := validation.ValidateHostname(configHostname)
|
||||
clientID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
|
||||
return nil, nil, errors.Wrap(err, "Invalid hostname")
|
||||
return nil, nil, errors.Wrap(err, "can't generate connector UUID")
|
||||
}
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msgf("Generated Connector ID: %s", clientID)
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
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})
|
||||
|
||||
var (
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelProperties
|
||||
)
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID.String()})
|
||||
|
||||
transportProtocol := c.String("protocol")
|
||||
protocolFetcher := edgediscovery.ProtocolPercentage
|
||||
needPQ := c.Bool("post-quantum")
|
||||
if needPQ {
|
||||
if FipsEnabled {
|
||||
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode")
|
||||
}
|
||||
// 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 := dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
|
||||
if needPQ {
|
||||
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
|
||||
}
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientID[:],
|
||||
Features: clientFeatures,
|
||||
Version: info.Version(),
|
||||
Arch: info.OSArch(),
|
||||
}
|
||||
cfg := config.GetConfiguration()
|
||||
if isNamedTunnel {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "can't generate connector UUID")
|
||||
}
|
||||
log.Info().Msgf("Generated Connector ID: %s", clientUUID)
|
||||
features := append(c.StringSlice("features"), defaultFeatures...)
|
||||
if c.IsSet(TunnelTokenFlag) {
|
||||
if transportProtocol == connection.AutoSelectFlag {
|
||||
protocolFetcher = func() (edgediscovery.ProtocolPercents, error) {
|
||||
// If the Tunnel is remotely managed and no protocol is set, we prefer QUIC, but still allow fall-back.
|
||||
preferQuic := []edgediscovery.ProtocolPercent{
|
||||
{
|
||||
Protocol: connection.QUIC.String(),
|
||||
Percentage: 100,
|
||||
},
|
||||
{
|
||||
Protocol: connection.HTTP2.String(),
|
||||
Percentage: 100,
|
||||
},
|
||||
}
|
||||
return preferQuic, nil
|
||||
}
|
||||
}
|
||||
log.Info().Msg("Will be fetching remotely managed configuration from Cloudflare API. Defaulting to protocol: quic")
|
||||
}
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
Features: dedup(features),
|
||||
Version: info.Version(),
|
||||
Arch: info.OSArch(),
|
||||
}
|
||||
ingressRules, err = ingress.ParseIngress(cfg)
|
||||
if err != nil && err != ingress.ErrNoIngressRules {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !ingressRules.IsEmpty() && c.IsSet("url") {
|
||||
return nil, nil, ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
} else {
|
||||
|
||||
originCertPath := c.String("origincert")
|
||||
originCertLog := log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCert, err := getOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
|
||||
classicTunnel = &connection.ClassicTunnelProperties{
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
UseReconnectToken: !isNamedTunnel && c.Bool("use-reconnect-token"),
|
||||
}
|
||||
ingressRules, err := ingress.ParseIngressFromConfigAndCLI(cfg, c, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Convert single-origin configuration into multi-origin configuration.
|
||||
if ingressRules.IsEmpty() {
|
||||
ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel)
|
||||
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, warpRoutingEnabled, namedTunnel, protocolFetcher, supervisor.ResolveTTL, log)
|
||||
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), c.Bool("post-quantum"), edgediscovery.ProtocolPercentage, connection.ResolveTTL, log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -317,28 +186,42 @@ func prepareTunnelConfig(
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
muxerConfig := &connection.MuxerConfig{
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
MaxHeartbeats: uint64(c.Int("heartbeat-count")),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
CompressionSetting: h2mux.CompressionSetting(uint64(c.Int("compression-quality"))),
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
}
|
||||
edgeIPVersion, err := parseConfigIPVersion(c.String("edge-ip-version"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
edgeBindAddr, err := parseConfigBindAddress(c.String("edge-bind-address"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := testIPBindable(edgeBindAddr); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid edge-bind-address %s: %v", edgeBindAddr, err)
|
||||
}
|
||||
edgeIPVersion, err = adjustIPVersionByBindAddress(edgeIPVersion, edgeBindAddr)
|
||||
if err != nil {
|
||||
// This is not a fatal error, we just overrode edgeIPVersion
|
||||
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"),
|
||||
OSArch: info.OSArch(),
|
||||
ClientID: clientID,
|
||||
ClientID: clientID.String(),
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
EdgeIPVersion: edgeIPVersion,
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
EdgeBindAddr: edgeBindAddr,
|
||||
HAConnections: c.Int(haConnectionsFlag),
|
||||
IncidentLookup: supervisor.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
LBPool: c.String("lb-pool"),
|
||||
@@ -348,13 +231,20 @@ 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,
|
||||
ClassicTunnel: classicTunnel,
|
||||
MuxerConfig: muxerConfig,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
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")),
|
||||
}
|
||||
packetConfig, err := newPacketConfig(c, log)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("ICMP proxy feature is disabled")
|
||||
} else {
|
||||
tunnelConfig.PacketConfig = packetConfig
|
||||
}
|
||||
orchestratorConfig := &orchestration.Config{
|
||||
Ingress: &ingressRules,
|
||||
@@ -384,10 +274,6 @@ func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
return period, nil
|
||||
}
|
||||
|
||||
func isWarpRoutingEnabled(warpConfig config.WarpRoutingConfig, isNamedTunnel bool) bool {
|
||||
return warpConfig.Enabled && isNamedTunnel
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
@@ -425,3 +311,192 @@ func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parseConfigBindAddress(ipstr string) (net.IP, error) {
|
||||
// Unspecified - it's fine
|
||||
if ipstr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
ip := net.ParseIP(ipstr)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid value for edge-bind-address: %s", ipstr)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func testIPBindable(ip net.IP) error {
|
||||
// "Unspecified" = let OS choose, so always bindable
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := &net.UDPAddr{IP: ip, Port: 0}
|
||||
listener, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func adjustIPVersionByBindAddress(ipVersion allregions.ConfigIPVersion, ip net.IP) (allregions.ConfigIPVersion, error) {
|
||||
if ip == nil {
|
||||
return ipVersion, nil
|
||||
}
|
||||
// https://pkg.go.dev/net#IP.To4: "If ip is not an IPv4 address, To4 returns nil."
|
||||
if ip.To4() != nil {
|
||||
if ipVersion == allregions.IPv6Only {
|
||||
return allregions.IPv4Only, fmt.Errorf("IPv4 bind address is specified, but edge-ip-version is IPv6")
|
||||
}
|
||||
return allregions.IPv4Only, nil
|
||||
} else {
|
||||
if ipVersion == allregions.IPv4Only {
|
||||
return allregions.IPv6Only, fmt.Errorf("IPv6 bind address is specified, but edge-ip-version is IPv4")
|
||||
}
|
||||
return allregions.IPv6Only, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newPacketConfig(c *cli.Context, logger *zerolog.Logger) (*ingress.GlobalRouterConfig, 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")
|
||||
}
|
||||
logger.Info().Msgf("ICMP proxy will use %s as source for IPv4", ipv4Src)
|
||||
|
||||
ipv6Src, zone, err := determineICMPv6Src(c.String("icmpv6-src"), logger, ipv4Src)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to determine IPv6 source address for ICMP proxy")
|
||||
}
|
||||
if zone != "" {
|
||||
logger.Info().Msgf("ICMP proxy will use %s in zone %s as source for IPv6", ipv6Src, zone)
|
||||
} else {
|
||||
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
|
||||
}
|
||||
|
||||
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, zone, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ingress.GlobalRouterConfig{
|
||||
ICMPRouter: icmpRouter,
|
||||
IPv4Src: ipv4Src,
|
||||
IPv6Src: ipv6Src,
|
||||
Zone: zone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) {
|
||||
if userDefinedSrc != "" {
|
||||
addr, err := netip.ParseAddr(userDefinedSrc)
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
if addr.Is4() {
|
||||
return addr, nil
|
||||
}
|
||||
return netip.Addr{}, fmt.Errorf("expect IPv4, but %s is IPv6", userDefinedSrc)
|
||||
}
|
||||
|
||||
addr, err := findLocalAddr(net.ParseIP("192.168.0.1"), 53)
|
||||
if err != nil {
|
||||
addr = netip.IPv4Unspecified()
|
||||
logger.Debug().Err(err).Msgf("Failed to determine the IPv4 for this machine. It will use %s to send/listen for ICMPv4 echo", addr)
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
type interfaceIP struct {
|
||||
name string
|
||||
ip net.IP
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return netip.Addr{}, "", err
|
||||
}
|
||||
if addr.Is6() {
|
||||
return addr, zone, nil
|
||||
}
|
||||
return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc)
|
||||
}
|
||||
|
||||
// Loop through all the interfaces, the preference is
|
||||
// 1. The interface where ipv4Src is in
|
||||
// 2. Interface with IPv6 address
|
||||
// 3. Unspecified interface
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return netip.IPv6Unspecified(), "", nil
|
||||
}
|
||||
|
||||
interfacesWithIPv6 := make([]interfaceIP, 0)
|
||||
for _, interf := range interfaces {
|
||||
interfaceAddrs, err := interf.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
foundIPv4SrcInterface := false
|
||||
for _, interfaceAddr := range interfaceAddrs {
|
||||
if ipnet, ok := interfaceAddr.(*net.IPNet); ok {
|
||||
ip := ipnet.IP
|
||||
if ip.Equal(ipv4Src.AsSlice()) {
|
||||
foundIPv4SrcInterface = true
|
||||
}
|
||||
if ip.To4() == nil {
|
||||
interfacesWithIPv6 = append(interfacesWithIPv6, interfaceIP{
|
||||
name: interf.Name,
|
||||
ip: ip,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Found the interface of ipv4Src. Loop through the addresses to see if there is an IPv6
|
||||
if foundIPv4SrcInterface {
|
||||
for _, interfaceAddr := range interfaceAddrs {
|
||||
if ipnet, ok := interfaceAddr.(*net.IPNet); ok {
|
||||
ip := ipnet.IP
|
||||
if ip.To4() == nil {
|
||||
addr, err := netip.ParseAddr(ip.String())
|
||||
if err == nil {
|
||||
return addr, interf.Name, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, interf := range interfacesWithIPv6 {
|
||||
addr, err := netip.ParseAddr(interf.ip.String())
|
||||
if err == nil {
|
||||
return addr, interf.name, nil
|
||||
}
|
||||
}
|
||||
logger.Debug().Err(err).Msgf("Failed to determine the IPv6 for this machine. It will use %s to send/listen for ICMPv6 echo", netip.IPv6Unspecified())
|
||||
|
||||
return netip.IPv6Unspecified(), "", nil
|
||||
}
|
||||
|
||||
// FindLocalAddr tries to dial UDP and returns the local address picked by the OS
|
||||
func findLocalAddr(dst net.IP, port int) (netip.Addr, error) {
|
||||
udpConn, err := net.DialUDP("udp", nil, &net.UDPAddr{
|
||||
IP: dst,
|
||||
Port: port,
|
||||
})
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
defer udpConn.Close()
|
||||
localAddrPort, err := netip.ParseAddrPort(udpConn.LocalAddr().String())
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
localAddr := localAddrPort.Addr()
|
||||
return localAddr, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
@@ -214,3 +215,23 @@ func getCertPoolSubjects(certPool *x509.CertPool) ([]*pkix.Name, error) {
|
||||
func isUnrecoverableError(err error) bool {
|
||||
return err != nil && err.Error() != "crypto/x509: system root pool is not available on Windows"
|
||||
}
|
||||
|
||||
func TestTestIPBindable(t *testing.T) {
|
||||
assert.Nil(t, testIPBindable(nil))
|
||||
|
||||
// Public services - if one of these IPs is on the machine, the test environment is too weird
|
||||
assert.NotNil(t, testIPBindable(net.ParseIP("8.8.8.8")))
|
||||
assert.NotNil(t, testIPBindable(net.ParseIP("1.1.1.1")))
|
||||
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, addr := range addrs {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
ip := addr.(*net.IPNet).IP
|
||||
assert.Nil(t, testIPBindable(ip))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -56,13 +57,13 @@ func newSearchByID(id uuid.UUID, c *cli.Context, log *zerolog.Logger, fs fileSys
|
||||
}
|
||||
|
||||
func (s searchByID) Path() (string, error) {
|
||||
originCertPath := s.c.String("origincert")
|
||||
originCertPath := s.c.String(credentials.OriginCertFlag)
|
||||
originCertLog := s.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Str("originCertPath", originCertPath).
|
||||
Logger()
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := findOriginCert(originCertPath, &originCertLog); err == nil {
|
||||
if originCertPath, err := credentials.FindOriginCert(originCertPath, &originCertLog); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
|
||||
if s.fs.validFilePath(filePath) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package tunnel
|
||||
|
||||
var FipsEnabled bool
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/token"
|
||||
)
|
||||
@@ -52,6 +53,7 @@ func login(c *cli.Context) error {
|
||||
|
||||
resourceData, err := token.RunTransfer(
|
||||
loginURL,
|
||||
"",
|
||||
"cert",
|
||||
"callback",
|
||||
callbackStoreURL,
|
||||
@@ -85,7 +87,7 @@ func checkForExistingCert() (string, bool, error) {
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
path := filepath.Join(configPath, config.DefaultCredentialFile)
|
||||
path := filepath.Join(configPath, credentials.DefaultCredentialFile)
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err == nil && fileInfo.Size() > 0 {
|
||||
return path, true, nil
|
||||
|
||||
@@ -73,6 +73,9 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
sc.c.Set("protocol", "quic")
|
||||
}
|
||||
|
||||
// Override the number of connections used. Quick tunnels shouldn't be used for production usage,
|
||||
// so, use a single connection instead.
|
||||
sc.c.Set(haConnectionsFlag, "1")
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ type subcommandContext struct {
|
||||
|
||||
// These fields should be accessed using their respective Getter
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
userCredential *credentials.User
|
||||
}
|
||||
|
||||
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
|
||||
@@ -56,65 +56,28 @@ func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder {
|
||||
return newSearchByID(tunnelID, sc.c, sc.log, sc.fs)
|
||||
}
|
||||
|
||||
type userCredential struct {
|
||||
cert *certutil.OriginCert
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) client() (cfapi.Client, error) {
|
||||
if sc.tunnelstoreClient != nil {
|
||||
return sc.tunnelstoreClient, nil
|
||||
}
|
||||
credential, err := sc.credential()
|
||||
cred, err := sc.credential()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", buildInfo.Version())
|
||||
client, err := cfapi.NewRESTClient(
|
||||
sc.c.String("api-url"),
|
||||
credential.cert.AccountID,
|
||||
credential.cert.ZoneID,
|
||||
credential.cert.ServiceKey,
|
||||
userAgent,
|
||||
sc.log,
|
||||
)
|
||||
|
||||
sc.tunnelstoreClient, err = cred.Client(sc.c.String("api-url"), buildInfo.UserAgent(), sc.log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sc.tunnelstoreClient = client
|
||||
return client, nil
|
||||
return sc.tunnelstoreClient, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) credential() (*userCredential, error) {
|
||||
func (sc *subcommandContext) credential() (*credentials.User, error) {
|
||||
if sc.userCredential == nil {
|
||||
originCertPath := sc.c.String("origincert")
|
||||
originCertLog := sc.log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCertPath, err := findOriginCert(originCertPath, &originCertLog)
|
||||
uc, err := credentials.Read(sc.c.String(credentials.OriginCertFlag), sc.log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
blocks, err := readOriginCert(originCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := certutil.DecodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
}
|
||||
|
||||
if cert.AccountID == "" {
|
||||
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
|
||||
}
|
||||
|
||||
sc.userCredential = &userCredential{
|
||||
cert: cert,
|
||||
certPath: originCertPath,
|
||||
return nil, err
|
||||
}
|
||||
sc.userCredential = uc
|
||||
}
|
||||
return sc.userCredential, nil
|
||||
}
|
||||
@@ -175,13 +138,13 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||
return nil, err
|
||||
}
|
||||
tunnelCredentials := connection.Credentials{
|
||||
AccountTag: credential.cert.AccountID,
|
||||
AccountTag: credential.AccountID(),
|
||||
TunnelSecret: tunnelSecret,
|
||||
TunnelID: tunnel.ID,
|
||||
}
|
||||
usedCertPath := false
|
||||
if credentialsFilePath == "" {
|
||||
originCertDir := filepath.Dir(credential.certPath)
|
||||
originCertDir := filepath.Dir(credential.CertPath())
|
||||
credentialsFilePath, err = tunnelFilePath(tunnelCredentials.TunnelID, originCertDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
)
|
||||
|
||||
type mockFileSystem struct {
|
||||
@@ -37,7 +38,7 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
log *zerolog.Logger
|
||||
fs fileSystem
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
userCredential *credentials.User
|
||||
}
|
||||
type args struct {
|
||||
tunnelID uuid.UUID
|
||||
@@ -249,7 +250,7 @@ func Test_subcommandContext_Delete(t *testing.T) {
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient *deleteMockTunnelStore
|
||||
userCredential *userCredential
|
||||
userCredential *credentials.User
|
||||
}
|
||||
type args struct {
|
||||
tunnelIDs []uuid.UUID
|
||||
|
||||
@@ -23,12 +23,12 @@ func (sc *subcommandContext) listVirtualNetworks(filter *cfapi.VnetFilter) ([]*c
|
||||
return client.ListVirtualNetworks(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
|
||||
func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID, force bool) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.DeleteVirtualNetwork(vnetId)
|
||||
return client.DeleteVirtualNetwork(vnetId, force)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates cfapi.UpdateVirtualNetwork) error {
|
||||
|
||||
@@ -95,14 +95,6 @@ var (
|
||||
Usage: "Inverts the sort order of the tunnel list.",
|
||||
EnvVars: []string{"TUNNEL_LIST_INVERT_SORT"},
|
||||
}
|
||||
forceFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "By default, if a tunnel is currently being run from a cloudflared, you can't " +
|
||||
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
|
||||
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
|
||||
"tunnels, you can do so with Cloudflare's Load Balancer product.",
|
||||
})
|
||||
featuresFlag = altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
|
||||
Name: "features",
|
||||
Aliases: []string{"F"},
|
||||
@@ -140,6 +132,13 @@ var (
|
||||
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
|
||||
Hidden: true,
|
||||
})
|
||||
postQuantumFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "post-quantum",
|
||||
Usage: "When given creates an experimental post-quantum secure tunnel",
|
||||
Aliases: []string{"pq"},
|
||||
EnvVars: []string{"TUNNEL_POST_QUANTUM"},
|
||||
Hidden: FipsEnabled,
|
||||
})
|
||||
sortInfoByFlag = &cli.StringFlag{
|
||||
Name: "sort-by",
|
||||
Value: "createdAt",
|
||||
@@ -169,6 +168,16 @@ var (
|
||||
Usage: "Base64 encoded secret to set for the tunnel. The decoded secret must be at least 32 bytes long. If not specified, a random 32-byte secret will be generated.",
|
||||
EnvVars: []string{"TUNNEL_CREATE_SECRET"},
|
||||
}
|
||||
icmpv4SrcFlag = &cli.StringFlag{
|
||||
Name: "icmpv4-src",
|
||||
Usage: "Source address to send/receive ICMPv4 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to 0.0.0.0.",
|
||||
EnvVars: []string{"TUNNEL_ICMPV4_SRC"},
|
||||
}
|
||||
icmpv6SrcFlag = &cli.StringFlag{
|
||||
Name: "icmpv6-src",
|
||||
Usage: "Source address and the interface name to send/receive ICMPv6 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to ::.",
|
||||
EnvVars: []string{"TUNNEL_ICMPV6_SRC"},
|
||||
}
|
||||
)
|
||||
|
||||
func buildCreateCommand() *cli.Command {
|
||||
@@ -599,12 +608,14 @@ func renderOutput(format string, v interface{}) error {
|
||||
|
||||
func buildRunCommand() *cli.Command {
|
||||
flags := []cli.Flag{
|
||||
forceFlag,
|
||||
credentialsFileFlag,
|
||||
credentialsContentsFlag,
|
||||
postQuantumFlag,
|
||||
selectProtocolFlag,
|
||||
featuresFlag,
|
||||
tunnelTokenFlag,
|
||||
icmpv4SrcFlag,
|
||||
icmpv6SrcFlag,
|
||||
}
|
||||
flags = append(flags, configureProxyFlags(false)...)
|
||||
return &cli.Command{
|
||||
@@ -612,7 +623,7 @@ func buildRunCommand() *cli.Command {
|
||||
Action: cliutil.ConfiguredAction(runCommand),
|
||||
Usage: "Proxy a local web server by running the given tunnel",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]",
|
||||
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
|
||||
Description: `Runs the tunnel identified by name or UUID, creating highly available connections
|
||||
between your server and the Cloudflare edge. You can provide name or UUID of tunnel to run either as the
|
||||
last command line argument or in the configuration file using "tunnel: TUNNEL".
|
||||
|
||||
@@ -763,7 +774,7 @@ func tokenCommand(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s", encodedToken)
|
||||
fmt.Println(encodedToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -801,7 +812,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||
Name: "lb",
|
||||
Action: cliutil.ConfiguredAction(routeLbCommand),
|
||||
Usage: "Use this tunnel as a load balancer origin, creating pool and load balancer if necessary",
|
||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME] [LB-POOL]",
|
||||
UsageText: "cloudflared tunnel route lb [TUNNEL] [HOSTNAME] [LB-POOL-NAME]",
|
||||
Description: `Creates Load Balancer with an origin pool that points to the tunnel.`,
|
||||
},
|
||||
buildRouteIPSubcommand(),
|
||||
@@ -921,7 +932,7 @@ func commandHelpTemplate() string {
|
||||
for _, f := range configureCloudflaredFlags(false) {
|
||||
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
|
||||
}
|
||||
for _, f := range configureLoggingFlags(false) {
|
||||
for _, f := range cliutil.ConfigureLoggingFlags(false) {
|
||||
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
|
||||
}
|
||||
const template = `NAME:
|
||||
|
||||
@@ -33,6 +33,12 @@ var (
|
||||
Aliases: []string{"c"},
|
||||
Usage: "A new comment describing the purpose of the virtual network.",
|
||||
}
|
||||
vnetForceDeleteFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Force the deletion of the virtual network even if it is being relied upon by other resources. Those" +
|
||||
"resources will either be deleted (e.g. IP Routes) or moved to the current default virutal network.",
|
||||
}
|
||||
)
|
||||
|
||||
func buildVirtualNetworkSubcommand(hidden bool) *cli.Command {
|
||||
@@ -82,6 +88,7 @@ be the current default.`,
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] network delete VIRTUAL_NETWORK",
|
||||
Description: `Deletes the virtual network (given its ID or name). This is only possible if that virtual network is unused.
|
||||
A virtual network may be used by IP routes or by WARP devices.`,
|
||||
Flags: []cli.Flag{vnetForceDeleteFlag},
|
||||
Hidden: hidden,
|
||||
},
|
||||
{
|
||||
@@ -188,7 +195,7 @@ func deleteVirtualNetworkCommand(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.NArg() != 1 {
|
||||
if c.NArg() < 1 {
|
||||
return errors.New("You must supply exactly one argument, either the ID or name of the virtual network to delete")
|
||||
}
|
||||
|
||||
@@ -198,7 +205,12 @@ func deleteVirtualNetworkCommand(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sc.deleteVirtualNetwork(vnetId); err != nil {
|
||||
forceDelete := false
|
||||
if c.IsSet(vnetForceDeleteFlag.Name) {
|
||||
forceDelete = c.Bool(vnetForceDeleteFlag.Name)
|
||||
}
|
||||
|
||||
if err := sc.deleteVirtualNetwork(vnetId, forceDelete); err != nil {
|
||||
return errors.Wrap(err, "API error")
|
||||
}
|
||||
fmt.Printf("Successfully deleted virtual network '%s'\n", input)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
@@ -95,11 +96,24 @@ func CheckForUpdate(options updateOptions) (CheckResult, error) {
|
||||
url = StagingUpdateURL
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
cfdPath = encodeWindowsPath(cfdPath)
|
||||
}
|
||||
|
||||
s := NewWorkersService(version, 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.
|
||||
targetPath := strings.Replace(path, "Program Files (x86)", "PROGRA~2", -1)
|
||||
// This is to do the same in 32 bit systems. We do this second so that the first
|
||||
// replace is for x86 dirs.
|
||||
targetPath = strings.Replace(targetPath, "Program Files", "PROGRA~1", -1)
|
||||
return targetPath
|
||||
}
|
||||
|
||||
func applyUpdate(options updateOptions, update CheckResult) UpdateOutcome {
|
||||
if update.Version() == "" || options.updateDisabled {
|
||||
|
||||
@@ -25,14 +25,13 @@ const (
|
||||
// rename cloudflared.exe.new to cloudflared.exe
|
||||
// delete cloudflared.exe.old
|
||||
// start the service
|
||||
// delete the batch file
|
||||
windowsUpdateCommandTemplate = `@echo off
|
||||
sc stop cloudflared >nul 2>&1
|
||||
// exit with code 0 if we've reached this point indicating success.
|
||||
windowsUpdateCommandTemplate = `sc stop cloudflared >nul 2>&1
|
||||
rename "{{.TargetPath}}" {{.OldName}}
|
||||
rename "{{.NewPath}}" {{.BinaryName}}
|
||||
del "{{.OldPath}}"
|
||||
sc start cloudflared >nul 2>&1
|
||||
del {{.BatchName}}`
|
||||
exit /b 0`
|
||||
batchFileName = "cfd_update.bat"
|
||||
)
|
||||
|
||||
@@ -214,8 +213,9 @@ func isValidChecksum(checksum, filePath string) error {
|
||||
// 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 {
|
||||
os.Remove(batchFileName) //remove any failed updates before download
|
||||
f, err := os.Create(batchFileName)
|
||||
batchFilePath := filepath.Join(filepath.Dir(targetPath), batchFileName)
|
||||
os.Remove(batchFilePath) //remove any failed updates before download
|
||||
f, err := os.Create(batchFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -241,6 +241,16 @@ func writeBatchFile(targetPath string, newPath string, oldPath string) error {
|
||||
|
||||
// run each OS command for windows
|
||||
func runWindowsBatch(batchFile string) error {
|
||||
cmd := exec.Command("cmd", "/c", batchFile)
|
||||
return cmd.Start()
|
||||
defer os.Remove(batchFile)
|
||||
cmd := exec.Command("cmd", "/C", batchFile)
|
||||
_, err := cmd.Output()
|
||||
// Remove the batch file we created. Don't let this interfere with the error
|
||||
// we report.
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return fmt.Errorf("Error during update : %s;", string(exitError.Stderr))
|
||||
}
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
cloudflared_binary: "cloudflared"
|
||||
tunnel: "3d539f97-cd3a-4d8e-c33b-65e9099c7a8d"
|
||||
credentials_file: "/Users/tunnel/.cloudflared/3d539f97-cd3a-4d8e-c33b-65e9099c7a8d.json"
|
||||
classic_hostname: "classic-tunnel-component-tests.example.com"
|
||||
origincert: "/Users/tunnel/.cloudflared/cert.pem"
|
||||
ingress:
|
||||
- hostname: named-tunnel-component-tests.example.com
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -30,6 +30,7 @@ class NamedTunnelBaseConfig(BaseConfig):
|
||||
tunnel: str = None
|
||||
credentials_file: str = None
|
||||
ingress: list = None
|
||||
hostname: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.tunnel is None:
|
||||
@@ -41,8 +42,10 @@ class NamedTunnelBaseConfig(BaseConfig):
|
||||
|
||||
def merge_config(self, additional):
|
||||
config = super(NamedTunnelBaseConfig, self).merge_config(additional)
|
||||
config['tunnel'] = self.tunnel
|
||||
config['credentials-file'] = self.credentials_file
|
||||
if 'tunnel' not in config:
|
||||
config['tunnel'] = self.tunnel
|
||||
if 'credentials-file' not in config:
|
||||
config['credentials-file'] = self.credentials_file
|
||||
# In some cases we want to override default ingress, such as in config tests
|
||||
if 'ingress' not in config:
|
||||
config['ingress'] = self.ingress
|
||||
@@ -61,7 +64,7 @@ class NamedTunnelConfig(NamedTunnelBaseConfig):
|
||||
self.merge_config(additional_config))
|
||||
|
||||
def get_url(self):
|
||||
return "https://" + self.ingress[0]['hostname']
|
||||
return "https://" + self.hostname
|
||||
|
||||
def base_config(self):
|
||||
config = self.full_config.copy()
|
||||
@@ -84,28 +87,9 @@ class NamedTunnelConfig(NamedTunnelBaseConfig):
|
||||
def get_credentials_json(self):
|
||||
with open(self.credentials_file) as json_file:
|
||||
return json.load(json_file)
|
||||
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClassicTunnelBaseConfig(BaseConfig):
|
||||
hostname: str = None
|
||||
origincert: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.hostname is None:
|
||||
raise TypeError("Field tunnel is not set")
|
||||
if self.origincert is None:
|
||||
raise TypeError("Field credentials_file is not set")
|
||||
|
||||
def merge_config(self, additional):
|
||||
config = super(ClassicTunnelBaseConfig, self).merge_config(additional)
|
||||
config['hostname'] = self.hostname
|
||||
config['origincert'] = self.origincert
|
||||
return config
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClassicTunnelConfig(ClassicTunnelBaseConfig):
|
||||
class QuickTunnelConfig(BaseConfig):
|
||||
full_config: dict = None
|
||||
additional_config: InitVar[dict] = {}
|
||||
|
||||
@@ -115,10 +99,6 @@ class ClassicTunnelConfig(ClassicTunnelBaseConfig):
|
||||
object.__setattr__(self, 'full_config',
|
||||
self.merge_config(additional_config))
|
||||
|
||||
def get_url(self):
|
||||
return "https://" + self.hostname
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProxyDnsConfig(BaseConfig):
|
||||
full_config = {
|
||||
|
||||
@@ -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
|
||||
@@ -5,14 +5,14 @@ from time import sleep
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from config import NamedTunnelConfig, ClassicTunnelConfig, ProxyDnsConfig
|
||||
from config import NamedTunnelConfig, ProxyDnsConfig, QuickTunnelConfig
|
||||
from constants import BACKOFF_SECS, PROXY_DNS_PORT
|
||||
from util import LOGGER
|
||||
|
||||
|
||||
class CfdModes(Enum):
|
||||
NAMED = auto()
|
||||
CLASSIC = auto()
|
||||
QUICK = auto()
|
||||
PROXY_DNS = auto()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ def component_tests_config():
|
||||
config = yaml.safe_load(stream)
|
||||
LOGGER.info(f"component tests base config {config}")
|
||||
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True):
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True, provide_ingress=True):
|
||||
if run_proxy_dns:
|
||||
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
|
||||
# So we run all tests with it.
|
||||
@@ -36,18 +36,25 @@ def component_tests_config():
|
||||
additional_config.pop("proxy-dns", None)
|
||||
additional_config.pop("proxy-dns-port", None)
|
||||
|
||||
# Allows the ingress rules to be omitted from the provided config
|
||||
ingress = []
|
||||
if provide_ingress:
|
||||
ingress = config['ingress']
|
||||
|
||||
# Provide the hostname to allow routing to the tunnel even if the ingress rule isn't defined in the config
|
||||
hostname = config['ingress'][0]['hostname']
|
||||
|
||||
if cfd_mode is CfdModes.NAMED:
|
||||
return NamedTunnelConfig(additional_config=additional_config,
|
||||
cloudflared_binary=config['cloudflared_binary'],
|
||||
tunnel=config['tunnel'],
|
||||
credentials_file=config['credentials_file'],
|
||||
ingress=config['ingress'])
|
||||
elif cfd_mode is CfdModes.CLASSIC:
|
||||
return ClassicTunnelConfig(
|
||||
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'],
|
||||
hostname=config['classic_hostname'], origincert=config['origincert'])
|
||||
ingress=ingress,
|
||||
hostname=hostname)
|
||||
elif cfd_mode is CfdModes.PROXY_DNS:
|
||||
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
|
||||
elif cfd_mode is CfdModes.QUICK:
|
||||
return QuickTunnelConfig(additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'])
|
||||
else:
|
||||
raise Exception(f"Unknown cloudflared mode {cfd_mode}")
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
METRICS_PORT = 51000
|
||||
MAX_RETRIES = 5
|
||||
BACKOFF_SECS = 7
|
||||
MAX_LOG_LINES = 50
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
|
||||
|
||||
|
||||
def protocols():
|
||||
return ["h2mux", "http2", "quic"]
|
||||
return ["http2", "quic"]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
cloudflare==2.8.15
|
||||
flaky==3.7.0
|
||||
pytest==6.2.2
|
||||
pytest==7.3.1
|
||||
pytest-asyncio==0.21.0
|
||||
pyyaml==5.4.1
|
||||
requests==2.25.1
|
||||
retrying==1.3.3
|
||||
requests==2.28.2
|
||||
retrying==1.3.4
|
||||
websockets==11.0.1
|
||||
|
||||
@@ -81,12 +81,6 @@ def create_dns(config, hostname, type, content):
|
||||
)
|
||||
|
||||
|
||||
def create_classic_dns(config, random_uuid):
|
||||
classic_hostname = "classic-" + random_uuid + "." + config["zone_domain"]
|
||||
create_dns(config, classic_hostname, "AAAA", "fd10:aec2:5dae::")
|
||||
return classic_hostname
|
||||
|
||||
|
||||
def create_named_dns(config, random_uuid):
|
||||
hostname = "named-" + random_uuid + "." + config["zone_domain"]
|
||||
create_dns(config, hostname, "CNAME", config["tunnel"] + ".cfargotunnel.com")
|
||||
@@ -119,7 +113,6 @@ def create():
|
||||
Creates the necessary resources for the components test to run.
|
||||
- Creates a named tunnel with a random name.
|
||||
- Creates a random CNAME DNS entry for that tunnel.
|
||||
- Creates a random AAAA DNS entry for a classic tunnel.
|
||||
|
||||
Those created resources are added to the config (obtained from an environment variable).
|
||||
The resulting configuration is persisted for the tests to use.
|
||||
@@ -129,7 +122,6 @@ def create():
|
||||
|
||||
random_uuid = str(uuid.uuid4())
|
||||
config["tunnel"] = create_tunnel(config, origincert_path, random_uuid)
|
||||
config["classic_hostname"] = create_classic_dns(config, random_uuid)
|
||||
config["ingress"] = [
|
||||
{
|
||||
"hostname": create_named_dns(config, random_uuid),
|
||||
@@ -150,7 +142,6 @@ def cleanup():
|
||||
"""
|
||||
config = get_config_from_file()
|
||||
delete_tunnel(config)
|
||||
delete_dns(config, config["classic_hostname"])
|
||||
delete_dns(config, config["ingress"][0]["hostname"])
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from constants import MAX_LOG_LINES
|
||||
from util import start_cloudflared, wait_tunnel_ready, send_requests
|
||||
|
||||
# Rolling logger rotate log files after 1 MB
|
||||
@@ -11,14 +12,24 @@ expect_message = "Starting Hello"
|
||||
|
||||
|
||||
def assert_log_to_terminal(cloudflared):
|
||||
stderr = cloudflared.stderr.read(1500)
|
||||
assert expect_message.encode() in stderr, f"{stderr} doesn't contain {expect_message}"
|
||||
for _ in range(0, MAX_LOG_LINES):
|
||||
line = cloudflared.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
if expect_message.encode() in line:
|
||||
return
|
||||
raise Exception(f"terminal log doesn't contain {expect_message}")
|
||||
|
||||
|
||||
def assert_log_in_file(file):
|
||||
with open(file, "r") as f:
|
||||
log = f.read(2000)
|
||||
assert expect_message in log, f"{log} doesn't contain {expect_message}"
|
||||
for _ in range(0, MAX_LOG_LINES):
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
if expect_message in line:
|
||||
return
|
||||
raise Exception(f"log file doesn't contain {expect_message}")
|
||||
|
||||
|
||||
def assert_json_log(file):
|
||||
@@ -63,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)
|
||||
|
||||
@@ -74,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)
|
||||
@@ -87,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,17 @@
|
||||
from util import LOGGER, nofips, start_cloudflared, wait_tunnel_ready
|
||||
|
||||
|
||||
@nofips
|
||||
class TestPostQuantum:
|
||||
def _extra_config(self):
|
||||
config = {
|
||||
"protocol": "quic",
|
||||
}
|
||||
return config
|
||||
|
||||
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_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=1)
|
||||
@@ -12,18 +12,12 @@ class TestProxyDns:
|
||||
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_with_classic_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
|
||||
|
||||
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
|
||||
|
||||
def test_classic_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=False)
|
||||
|
||||
|
||||
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
|
||||
expect_proxy_dns = run_proxy_dns
|
||||
@@ -31,12 +25,8 @@ 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.CLASSIC:
|
||||
expect_tunnel = True
|
||||
pre_args = []
|
||||
args = []
|
||||
elif cfd_mode == CfdModes.PROXY_DNS:
|
||||
expect_proxy_dns = True
|
||||
pre_args = []
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
from conftest import CfdModes
|
||||
from constants import METRICS_PORT
|
||||
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_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
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_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
url = get_quicktunnel_url()
|
||||
send_requests(url+"/ready", 3, True)
|
||||
|
||||
def test_quick_tunnel_proxy_dns_url(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
|
||||
LOGGER.debug(config)
|
||||
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], expect_success=False)
|
||||
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `hello-world`"
|
||||
|
||||
def test_quick_tunnel_proxy_dns_hello_world(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
|
||||
LOGGER.debug(config)
|
||||
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--hello-world"], expect_success=False)
|
||||
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `url`"
|
||||
@@ -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,17 +29,10 @@ 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)
|
||||
|
||||
def test_classic_reconnect(self, tmp_path, component_tests_config):
|
||||
extra_config = copy.copy(self.extra_config)
|
||||
extra_config["hello-world"] = True
|
||||
config = component_tests_config(additional_config=extra_config, cfd_mode=CfdModes.CLASSIC)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
self.assert_reconnect(config, cloudflared, 1)
|
||||
|
||||
def send_reconnect(self, cloudflared, secs):
|
||||
# Although it is recommended to use the Popen.communicate method, we cannot
|
||||
# use it because it blocks on reading stdout and stderr until EOF is reached
|
||||
@@ -47,17 +40,12 @@ class TestReconnect:
|
||||
cloudflared.stdin.flush()
|
||||
|
||||
def assert_reconnect(self, config, cloudflared, repeat):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=self.default_ha_conns)
|
||||
for _ in range(repeat):
|
||||
for i in range(self.default_ha_conns):
|
||||
for _ in range(self.default_ha_conns):
|
||||
self.send_reconnect(cloudflared, self.default_reconnect_secs)
|
||||
expect_connections = self.default_ha_conns-i-1
|
||||
if expect_connections > 0:
|
||||
# Don't check if tunnel returns 200 here because there is a race condition between wait_tunnel_ready
|
||||
# retrying to get 200 response and reconnecting
|
||||
wait_tunnel_ready(require_min_connections=expect_connections)
|
||||
else:
|
||||
check_tunnel_not_connected()
|
||||
|
||||
check_tunnel_not_connected()
|
||||
sleep(self.default_reconnect_secs * 2)
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=self.default_ha_conns)
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/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('{"type": "start_streaming"}')
|
||||
# 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
|
||||
}
|
||||
}))
|
||||
# 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()
|
||||
@@ -56,7 +56,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()
|
||||
@@ -76,7 +76,7 @@ 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():
|
||||
self.terminate_by_signal(cloudflared, signal)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/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
|
||||
|
||||
class TestTunnel:
|
||||
'''Test tunnels with no ingress rules from config.yaml but ingress rules from CLI only'''
|
||||
|
||||
def test_tunnel_hello_world(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=1)
|
||||
|
||||
"""
|
||||
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"
|
||||
|
||||
|
||||
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_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):
|
||||
'''
|
||||
Running a tunnel with no ingress rules provided from either config.yaml or CLI will still work but return 503
|
||||
for all incoming requests.
|
||||
'''
|
||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||
LOGGER.debug(config)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True):
|
||||
wait_tunnel_ready(require_min_connections=1)
|
||||
resp = send_request(config.get_url()+"/")
|
||||
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
|
||||
resp = send_request(config.get_url()+"/test")
|
||||
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
|
||||
|
||||
|
||||
@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)
|
||||
+28
-7
@@ -9,16 +9,24 @@ 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 select_platform(plat):
|
||||
return pytest.mark.skipif(
|
||||
platform.system() != plat, reason=f"Only runs on {plat}")
|
||||
|
||||
def fips_enabled():
|
||||
env_fips = os.getenv("COMPONENT_TESTS_FIPS")
|
||||
return env_fips is not None and env_fips != "0"
|
||||
|
||||
nofips = pytest.mark.skipif(
|
||||
fips_enabled(), reason=f"Only runs without FIPS (COMPONENT_TESTS_FIPS=0)")
|
||||
|
||||
def write_config(directory, config):
|
||||
config_path = directory / "config.yml"
|
||||
@@ -28,7 +36,7 @@ def write_config(directory, config):
|
||||
|
||||
|
||||
def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel"], new_process=False,
|
||||
allow_input=False, capture_output=True, root=False, skip_config_flag=False):
|
||||
allow_input=False, capture_output=True, root=False, skip_config_flag=False, expect_success=True):
|
||||
|
||||
config_path = None
|
||||
if not skip_config_flag:
|
||||
@@ -39,8 +47,7 @@ def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel
|
||||
if new_process:
|
||||
return run_cloudflared_background(cmd, allow_input, capture_output)
|
||||
# By setting check=True, it will raise an exception if the process exits with non-zero exit code
|
||||
return subprocess.run(cmd, check=True, capture_output=capture_output)
|
||||
|
||||
return subprocess.run(cmd, check=expect_success, capture_output=capture_output)
|
||||
|
||||
def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
|
||||
cmd = []
|
||||
@@ -70,7 +77,18 @@ def run_cloudflared_background(cmd, allow_input, capture_output):
|
||||
cfd.terminate()
|
||||
if capture_output:
|
||||
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
|
||||
|
||||
|
||||
def get_quicktunnel_url():
|
||||
quicktunnel_url = f'http://localhost:{METRICS_PORT}/quicktunnel'
|
||||
with requests.Session() as s:
|
||||
resp = send_request(s, quicktunnel_url, True)
|
||||
|
||||
hostname = resp.json()["hostname"]
|
||||
assert hostname, \
|
||||
f"Quicktunnel endpoint returned {hostname} but we expected a url"
|
||||
|
||||
return f"https://{hostname}"
|
||||
|
||||
def wait_tunnel_ready(tunnel_url=None, require_min_connections=1, cfd_logs=None):
|
||||
try:
|
||||
@@ -88,13 +106,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):
|
||||
@@ -108,13 +127,15 @@ def _log_cloudflared_logs(cfd_logs):
|
||||
LOGGER.warning(line)
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES * BACKOFF_SECS, wait_fixed=1000)
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
def check_tunnel_not_connected():
|
||||
url = f'http://localhost:{METRICS_PORT}/ready'
|
||||
|
||||
try:
|
||||
resp = requests.get(url, timeout=1)
|
||||
resp = requests.get(url, timeout=BACKOFF_SECS)
|
||||
assert resp.status_code == 503, f"Expect {url} returns 503, got {resp.status_code}"
|
||||
assert resp.json()[
|
||||
"readyConnections"] == 0, "Expected all connections to be terminated (pending reconnect)"
|
||||
# cloudflared might already terminate
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
LOGGER.warning(f"Failed to connect to {url}, error: {e}")
|
||||
|
||||
+15
-3
@@ -39,8 +39,6 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCredentialFile = "cert.pem"
|
||||
|
||||
// BastionFlag is to enable bastion, or jump host, operation
|
||||
BastionFlag = "bastion"
|
||||
)
|
||||
@@ -229,6 +227,19 @@ type OriginRequestConfig struct {
|
||||
IPRules []IngressIPRule `yaml:"ipRules" json:"ipRules,omitempty"`
|
||||
// Attempt to connect to origin with HTTP/2
|
||||
Http2Origin *bool `yaml:"http2Origin" json:"http2Origin,omitempty"`
|
||||
// Access holds all access related configs
|
||||
Access *AccessConfig `yaml:"access" json:"access,omitempty"`
|
||||
}
|
||||
|
||||
type AccessConfig struct {
|
||||
// Required when set to true will fail every request that does not arrive through an access authenticated endpoint.
|
||||
Required bool `yaml:"required" json:"required,omitempty"`
|
||||
|
||||
// TeamName is the organization team name to get the public key certificates for.
|
||||
TeamName string `yaml:"teamName" json:"teamName"`
|
||||
|
||||
// AudTag is the AudTag to verify access JWT against.
|
||||
AudTag []string `yaml:"audTag" json:"audTag"`
|
||||
}
|
||||
|
||||
type IngressIPRule struct {
|
||||
@@ -378,7 +389,8 @@ func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSe
|
||||
log.Debug().Msgf("Loading configuration from %s", configFile)
|
||||
file, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// If does not exist and config file was not specificly specified then return ErrNoConfigFile found.
|
||||
if os.IsNotExist(err) && !c.IsSet("config") {
|
||||
err = ErrNoConfigFile
|
||||
}
|
||||
return nil, "", err
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -24,14 +25,22 @@ const (
|
||||
LogFieldConnIndex = "connIndex"
|
||||
MaxGracePeriod = time.Minute * 3
|
||||
MaxConcurrentStreams = math.MaxUint32
|
||||
|
||||
contentTypeHeader = "content-type"
|
||||
sseContentType = "text/event-stream"
|
||||
grpcContentType = "application/grpc"
|
||||
)
|
||||
|
||||
var switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
var (
|
||||
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
flushableContentTypes = []string{sseContentType, grpcContentType}
|
||||
)
|
||||
|
||||
type Orchestrator interface {
|
||||
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
|
||||
GetConfigJSON() ([]byte, error)
|
||||
GetOriginProxy() (OriginProxy, error)
|
||||
WarpRoutingEnabled() (enabled bool)
|
||||
}
|
||||
|
||||
type NamedTunnelProperties struct {
|
||||
@@ -134,6 +143,7 @@ type TCPRequest struct {
|
||||
LBProbe bool
|
||||
FlowID string
|
||||
CfTraceID string
|
||||
ConnIndex uint8
|
||||
}
|
||||
|
||||
// ReadWriteAcker is a readwriter with the ability to Acknowledge to the downstream (edge) that the origin has
|
||||
@@ -174,6 +184,7 @@ func (h *HTTPResponseReadWriteAcker) AckConnection(tracePropagation string) erro
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
ContentLength: -1,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if secWebsocketKey := h.req.Header.Get("Sec-WebSocket-Key"); secWebsocketKey != "" {
|
||||
@@ -187,8 +198,55 @@ func (h *HTTPResponseReadWriteAcker) AckConnection(tracePropagation string) erro
|
||||
return h.w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
}
|
||||
|
||||
// localProxyConnection emulates an incoming connection to cloudflared as a net.Conn.
|
||||
// Used when handling a "hijacked" connection from connection.ResponseWriter
|
||||
type localProxyConnection struct {
|
||||
io.ReadWriteCloser
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) Read(b []byte) (int, error) {
|
||||
return c.ReadWriteCloser.Read(b)
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) Write(b []byte) (int, error) {
|
||||
return c.ReadWriteCloser.Write(b)
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) Close() error {
|
||||
return c.ReadWriteCloser.Close()
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) LocalAddr() net.Addr {
|
||||
// Unused LocalAddr
|
||||
return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""}
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) RemoteAddr() net.Addr {
|
||||
// Unused RemoteAddr
|
||||
return &net.TCPAddr{IP: net.IPv6loopback, Port: 0, Zone: ""}
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) SetDeadline(t time.Time) error {
|
||||
// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) SetReadDeadline(t time.Time) error {
|
||||
// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *localProxyConnection) SetWriteDeadline(t time.Time) error {
|
||||
// ignored since we can't set the read/write Deadlines for the tunnel back to origintunneld
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResponseWriter is the response path for a request back through cloudflared's tunnel.
|
||||
type ResponseWriter interface {
|
||||
WriteRespHeaders(status int, header http.Header) error
|
||||
AddTrailer(trailerName, trailerValue string)
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
io.Writer
|
||||
}
|
||||
|
||||
@@ -197,10 +255,18 @@ type ConnectedFuse interface {
|
||||
IsConnected() bool
|
||||
}
|
||||
|
||||
func IsServerSentEvent(headers http.Header) bool {
|
||||
if contentType := headers.Get("content-type"); contentType != "" {
|
||||
return strings.HasPrefix(strings.ToLower(contentType), "text/event-stream")
|
||||
// Helper method to let the caller know what content-types should require a flush on every
|
||||
// write to a ResponseWriter.
|
||||
func shouldFlush(headers http.Header) bool {
|
||||
if contentType := headers.Get(contentTypeHeader); contentType != "" {
|
||||
contentType = strings.ToLower(contentType)
|
||||
for _, c := range flushableContentTypes {
|
||||
if strings.HasPrefix(contentType, c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,11 @@ import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/stream"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
@@ -58,6 +57,10 @@ func (mcr *mockOrchestrator) GetOriginProxy() (OriginProxy, error) {
|
||||
return mcr.originProxy, nil
|
||||
}
|
||||
|
||||
func (mcr *mockOrchestrator) WarpRoutingEnabled() (enabled bool) {
|
||||
return true
|
||||
}
|
||||
|
||||
type mockOriginProxy struct{}
|
||||
|
||||
func (moc *mockOriginProxy) ProxyHTTP(
|
||||
@@ -138,7 +141,7 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
}()
|
||||
|
||||
originConn := &echoPipe{reader: readPipe, writer: writePipe}
|
||||
websocket.Stream(wsConn, originConn, &log)
|
||||
stream.Pipe(wsConn, originConn, &log)
|
||||
cancel()
|
||||
wsConn.Close()
|
||||
return nil
|
||||
@@ -176,7 +179,7 @@ func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
|
||||
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
|
||||
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
|
||||
websocket.Stream(wsConn, originConn, &log)
|
||||
stream.Pipe(wsConn, originConn, &log)
|
||||
cancel()
|
||||
wsConn.Close()
|
||||
return nil
|
||||
@@ -197,40 +200,3 @@ func (mcf mockConnectedFuse) Connected() {}
|
||||
func (mcf mockConnectedFuse) IsConnected() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestIsEventStream(t *testing.T) {
|
||||
tests := []struct {
|
||||
headers http.Header
|
||||
isEventStream bool
|
||||
}{
|
||||
{
|
||||
headers: newHeader("Content-Type", "text/event-stream"),
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
headers: newHeader("content-type", "text/event-stream"),
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
headers: newHeader("Content-Type", "text/event-stream; charset=utf-8"),
|
||||
isEventStream: true,
|
||||
},
|
||||
{
|
||||
headers: newHeader("Content-Type", "application/json"),
|
||||
isEventStream: false,
|
||||
},
|
||||
{
|
||||
headers: http.Header{},
|
||||
isEventStream: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.isEventStream, IsServerSentEvent(test.headers))
|
||||
}
|
||||
}
|
||||
|
||||
func newHeader(key, value string) http.Header {
|
||||
header := http.Header{}
|
||||
header.Add(key, value)
|
||||
return header
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
@@ -85,7 +85,7 @@ func (c *controlStream) ServeControlStream(
|
||||
return err
|
||||
}
|
||||
|
||||
c.observer.logServerInfo(c.connIndex, registrationDetails.Location, c.edgeAddress, fmt.Sprintf("Connection %s registered", registrationDetails.UUID))
|
||||
c.observer.logConnected(registrationDetails.UUID, c.connIndex, registrationDetails.Location, c.edgeAddress, c.protocol)
|
||||
c.observer.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location)
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
@@ -116,7 +116,11 @@ func (c *controlStream) waitForUnregister(ctx context.Context, rpcClient NamedTu
|
||||
|
||||
c.observer.sendUnregisteringEvent(c.connIndex)
|
||||
rpcClient.GracefulShutdown(ctx, c.gracePeriod)
|
||||
c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Unregistered tunnel connection")
|
||||
c.observer.log.Info().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Uint8(LogFieldConnIndex, c.connIndex).
|
||||
IPAddr(LogFieldIPAddress, c.edgeAddress).
|
||||
Msg("Unregistered tunnel connection")
|
||||
}
|
||||
|
||||
func (c *controlStream) IsStopped() bool {
|
||||
|
||||
@@ -27,6 +27,10 @@ func (e *EdgeQuicDialError) Error() string {
|
||||
return "failed to dial to edge with quic: " + e.Cause.Error()
|
||||
}
|
||||
|
||||
func (e *EdgeQuicDialError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// RegisterTunnel error from server
|
||||
type ServerRegisterTunnelError struct {
|
||||
Cause error
|
||||
|
||||
+1
-243
@@ -1,46 +1,17 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
muxerTimeout = 5 * time.Second
|
||||
openStreamTimeout = 30 * time.Second
|
||||
muxerTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type h2muxConnection struct {
|
||||
orchestrator Orchestrator
|
||||
gracePeriod time.Duration
|
||||
muxerConfig *MuxerConfig
|
||||
muxer *h2mux.Muxer
|
||||
// connectionID is only used by metrics, and prometheus requires labels to be string
|
||||
connIndexStr string
|
||||
connIndex uint8
|
||||
|
||||
observer *Observer
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
}
|
||||
|
||||
type MuxerConfig struct {
|
||||
HeartbeatInterval time.Duration
|
||||
MaxHeartbeats uint64
|
||||
@@ -59,216 +30,3 @@ func (mc *MuxerConfig) H2MuxerConfig(h h2mux.MuxedStreamHandler, log *zerolog.Lo
|
||||
CompressionQuality: mc.CompressionSetting,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTunnelHandler returns a TunnelHandler, origin LAN IP and error
|
||||
func NewH2muxConnection(
|
||||
orchestrator Orchestrator,
|
||||
gracePeriod time.Duration,
|
||||
muxerConfig *MuxerConfig,
|
||||
edgeConn net.Conn,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
log *zerolog.Logger,
|
||||
) (*h2muxConnection, error, bool) {
|
||||
h := &h2muxConnection{
|
||||
orchestrator: orchestrator,
|
||||
gracePeriod: gracePeriod,
|
||||
muxerConfig: muxerConfig,
|
||||
connIndexStr: uint8ToString(connIndex),
|
||||
connIndex: connIndex,
|
||||
observer: observer,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
log: log,
|
||||
}
|
||||
|
||||
// Establish a muxed connection with the edge
|
||||
// Client mux handshake with agent server
|
||||
muxer, err := h2mux.Handshake(edgeConn, edgeConn, *muxerConfig.H2MuxerConfig(h, observer.logTransport), h2mux.ActiveStreams)
|
||||
if err != nil {
|
||||
recoverable := isHandshakeErrRecoverable(err, connIndex, observer)
|
||||
return nil, err, recoverable
|
||||
}
|
||||
h.muxer = muxer
|
||||
return h, nil, false
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) ServeNamedTunnel(ctx context.Context, namedTunnel *NamedTunnelProperties, connOptions *tunnelpogs.ConnectionOptions, connectedFuse ConnectedFuse) error {
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
return h.serveMuxer(serveCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
if err := h.registerNamedTunnel(serveCtx, namedTunnel, connOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
connectedFuse.Connected()
|
||||
return nil
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
h.controlLoop(serveCtx, connectedFuse, true)
|
||||
return nil
|
||||
})
|
||||
|
||||
err := errGroup.Wait()
|
||||
if err == errMuxerStopped {
|
||||
if h.stoppedGracefully {
|
||||
return nil
|
||||
}
|
||||
h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unexpected muxer shutdown")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) ServeClassicTunnel(ctx context.Context, classicTunnel *ClassicTunnelProperties, credentialManager CredentialManager, registrationOptions *tunnelpogs.RegistrationOptions, connectedFuse ConnectedFuse) error {
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
return h.serveMuxer(serveCtx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() (err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
connectedFuse.Connected()
|
||||
}
|
||||
}()
|
||||
if classicTunnel.UseReconnectToken && connectedFuse.IsConnected() {
|
||||
err := h.reconnectTunnel(ctx, credentialManager, classicTunnel, registrationOptions)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
// log errors and proceed to RegisterTunnel
|
||||
h.observer.log.Err(err).
|
||||
Uint8(LogFieldConnIndex, h.connIndex).
|
||||
Msg("Couldn't reconnect connection. Re-registering it instead.")
|
||||
}
|
||||
return h.registerTunnel(ctx, credentialManager, classicTunnel, registrationOptions)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
h.controlLoop(serveCtx, connectedFuse, false)
|
||||
return nil
|
||||
})
|
||||
|
||||
err := errGroup.Wait()
|
||||
if err == errMuxerStopped {
|
||||
if h.stoppedGracefully {
|
||||
return nil
|
||||
}
|
||||
h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unexpected muxer shutdown")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) serveMuxer(ctx context.Context) error {
|
||||
// All routines should stop when muxer finish serving. When muxer is shutdown
|
||||
// gracefully, it doesn't return an error, so we need to return errMuxerShutdown
|
||||
// here to notify other routines to stop
|
||||
err := h.muxer.Serve(ctx)
|
||||
if err == nil {
|
||||
return errMuxerStopped
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) controlLoop(ctx context.Context, connectedFuse ConnectedFuse, isNamedTunnel bool) {
|
||||
updateMetricsTicker := time.NewTicker(h.muxerConfig.MetricsUpdateFreq)
|
||||
defer updateMetricsTicker.Stop()
|
||||
var shutdownCompleted <-chan struct{}
|
||||
for {
|
||||
select {
|
||||
case <-h.gracefulShutdownC:
|
||||
if connectedFuse.IsConnected() {
|
||||
h.unregister(isNamedTunnel)
|
||||
}
|
||||
h.stoppedGracefully = true
|
||||
h.gracefulShutdownC = nil
|
||||
shutdownCompleted = h.muxer.Shutdown()
|
||||
|
||||
case <-shutdownCompleted:
|
||||
return
|
||||
|
||||
case <-ctx.Done():
|
||||
// UnregisterTunnel blocks until the RPC call returns
|
||||
if !h.stoppedGracefully && connectedFuse.IsConnected() {
|
||||
h.unregister(isNamedTunnel)
|
||||
}
|
||||
h.muxer.Shutdown()
|
||||
// don't wait for shutdown to finish when context is closed, this is the hard termination path
|
||||
return
|
||||
|
||||
case <-updateMetricsTicker.C:
|
||||
h.observer.metrics.updateMuxerMetrics(h.connIndexStr, h.muxer.Metrics())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) newRPCStream(ctx context.Context, rpcName rpcName) (*h2mux.MuxedStream, error) {
|
||||
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
|
||||
defer openStreamCancel()
|
||||
stream, err := h.muxer.OpenRPCStream(openStreamCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
respWriter := &h2muxRespWriter{stream}
|
||||
|
||||
req, reqErr := h.newRequest(stream)
|
||||
if reqErr != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
return reqErr
|
||||
}
|
||||
|
||||
var sourceConnectionType = TypeHTTP
|
||||
if websocket.IsWebSocketUpgrade(req) {
|
||||
sourceConnectionType = TypeWebsocket
|
||||
}
|
||||
|
||||
originProxy, err := h.orchestrator.GetOriginProxy()
|
||||
if err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
return err
|
||||
}
|
||||
|
||||
err = originProxy.ProxyHTTP(respWriter, tracing.NewTracedHTTPRequest(req, h.log), sourceConnectionType == TypeWebsocket)
|
||||
if err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) newRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:8080", h2mux.MuxedStreamReader{MuxedStream: stream})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
|
||||
}
|
||||
err = H2RequestHeadersToH1Request(stream.Headers, req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid request received")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
type h2muxRespWriter struct {
|
||||
*h2mux.MuxedStream
|
||||
}
|
||||
|
||||
func (rp *h2muxRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
headers := H1ResponseToH2ResponseHeaders(status, header)
|
||||
headers = append(headers, h2mux.Header{Name: ResponseMetaHeader, Value: responseMetaHeaderOrigin})
|
||||
return rp.WriteHeaders(headers)
|
||||
}
|
||||
|
||||
func (rp *h2muxRespWriter) WriteErrorResponse() {
|
||||
_ = rp.WriteHeaders([]h2mux.Header{
|
||||
{Name: ":status", Value: "502"},
|
||||
{Name: ResponseMetaHeader, Value: responseMetaHeaderCfd},
|
||||
})
|
||||
_, _ = rp.Write([]byte("502 Bad Gateway"))
|
||||
}
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
)
|
||||
|
||||
var (
|
||||
testMuxerConfig = &MuxerConfig{
|
||||
HeartbeatInterval: time.Second * 5,
|
||||
MaxHeartbeats: 5,
|
||||
CompressionSetting: 0,
|
||||
MetricsUpdateFreq: time.Second * 5,
|
||||
}
|
||||
)
|
||||
|
||||
func newH2MuxConnection(t require.TestingT) (*h2muxConnection, *h2mux.Muxer) {
|
||||
edgeConn, originConn := net.Pipe()
|
||||
edgeMuxChan := make(chan *h2mux.Muxer)
|
||||
go func() {
|
||||
edgeMuxConfig := h2mux.MuxerConfig{
|
||||
Log: &log,
|
||||
Handler: h2mux.MuxedStreamFunc(func(stream *h2mux.MuxedStream) error {
|
||||
// we only expect RPC traffic in client->edge direction, provide minimal support for mocking
|
||||
require.True(t, stream.IsRPCStream())
|
||||
return stream.WriteHeaders([]h2mux.Header{
|
||||
{Name: ":status", Value: "200"},
|
||||
})
|
||||
}),
|
||||
}
|
||||
edgeMux, err := h2mux.Handshake(edgeConn, edgeConn, edgeMuxConfig, h2mux.ActiveStreams)
|
||||
require.NoError(t, err)
|
||||
edgeMuxChan <- edgeMux
|
||||
}()
|
||||
var connIndex = uint8(0)
|
||||
testObserver := NewObserver(&log, &log)
|
||||
h2muxConn, err, _ := NewH2muxConnection(testOrchestrator, testGracePeriod, testMuxerConfig, originConn, connIndex, testObserver, nil, &log)
|
||||
require.NoError(t, err)
|
||||
return h2muxConn, <-edgeMuxChan
|
||||
}
|
||||
|
||||
func TestServeStreamHTTP(t *testing.T) {
|
||||
tests := []testRequest{
|
||||
{
|
||||
name: "ok",
|
||||
endpoint: "/ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []byte(http.StatusText(http.StatusOK)),
|
||||
},
|
||||
{
|
||||
name: "large_file",
|
||||
endpoint: "/large_file",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: testLargeResp,
|
||||
},
|
||||
{
|
||||
name: "Bad request",
|
||||
endpoint: "/400",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedBody: []byte(http.StatusText(http.StatusBadRequest)),
|
||||
},
|
||||
{
|
||||
name: "Internal server error",
|
||||
endpoint: "/500",
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedBody: []byte(http.StatusText(http.StatusInternalServerError)),
|
||||
},
|
||||
{
|
||||
name: "Proxy error",
|
||||
endpoint: "/error",
|
||||
expectedStatus: http.StatusBadGateway,
|
||||
expectedBody: nil,
|
||||
isProxyError: true,
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h2muxConn, edgeMux := newH2MuxConnection(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := h2muxConn.serveMuxer(ctx)
|
||||
require.Error(t, err)
|
||||
}()
|
||||
|
||||
for _, test := range tests {
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: test.endpoint,
|
||||
},
|
||||
}
|
||||
stream, err := edgeMux.OpenStream(ctx, headers, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, hasHeader(stream, ":status", strconv.Itoa(test.expectedStatus)))
|
||||
|
||||
if test.isProxyError {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderCfd))
|
||||
} else {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
body := make([]byte, len(test.expectedBody))
|
||||
_, err = stream.Read(body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expectedBody, body)
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestServeStreamWS(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h2muxConn, edgeMux := newH2MuxConnection(t)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := h2muxConn.serveMuxer(ctx)
|
||||
require.Error(t, err)
|
||||
}()
|
||||
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: "/ws/echo",
|
||||
},
|
||||
{
|
||||
Name: "connection",
|
||||
Value: "upgrade",
|
||||
},
|
||||
{
|
||||
Name: "upgrade",
|
||||
Value: "websocket",
|
||||
},
|
||||
}
|
||||
|
||||
readPipe, writePipe := io.Pipe()
|
||||
stream, err := edgeMux.OpenStream(ctx, headers, readPipe)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, hasHeader(stream, ":status", strconv.Itoa(http.StatusSwitchingProtocols)))
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientBinary(writePipe, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
respBody, err := wsutil.ReadServerBinary(stream)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestGracefulShutdownH2Mux(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
h2muxConn, edgeMux := newH2MuxConnection(t)
|
||||
|
||||
shutdownC := make(chan struct{})
|
||||
unregisteredC := make(chan struct{})
|
||||
h2muxConn.gracefulShutdownC = shutdownC
|
||||
h2muxConn.newRPCClientFunc = func(_ context.Context, _ io.ReadWriteCloser, _ *zerolog.Logger) NamedTunnelRPCClient {
|
||||
return &mockNamedTunnelRPCClient{
|
||||
registered: nil,
|
||||
unregistered: unregisteredC,
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = h2muxConn.serveMuxer(ctx)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
h2muxConn.controlLoop(ctx, &mockConnectedFuse{}, true)
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
close(shutdownC)
|
||||
|
||||
select {
|
||||
case <-unregisteredC:
|
||||
break // ok
|
||||
case <-time.Tick(time.Second):
|
||||
assert.Fail(t, "timed out waiting for control loop to unregister")
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
|
||||
assert.True(t, h2muxConn.stoppedGracefully)
|
||||
assert.Nil(t, h2muxConn.gracefulShutdownC)
|
||||
}
|
||||
|
||||
func hasHeader(stream *h2mux.MuxedStream, name, val string) bool {
|
||||
for _, header := range stream.Headers {
|
||||
if header.Name == name && header.Value == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func benchmarkServeStreamHTTPSimple(b *testing.B, test testRequest) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h2muxConn, edgeMux := newH2MuxConnection(b)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
edgeMux.Serve(ctx)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := h2muxConn.serveMuxer(ctx)
|
||||
require.Error(b, err)
|
||||
}()
|
||||
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: test.endpoint,
|
||||
},
|
||||
}
|
||||
|
||||
body := make([]byte, len(test.expectedBody))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StartTimer()
|
||||
stream, openstreamErr := edgeMux.OpenStream(ctx, headers, nil)
|
||||
_, readBodyErr := stream.Read(body)
|
||||
b.StopTimer()
|
||||
|
||||
require.NoError(b, openstreamErr)
|
||||
assert.True(b, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
require.True(b, hasHeader(stream, ":status", strconv.Itoa(http.StatusOK)))
|
||||
require.NoError(b, readBodyErr)
|
||||
require.Equal(b, test.expectedBody, body)
|
||||
}
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkServeStreamHTTPSimple(b *testing.B) {
|
||||
test := testRequest{
|
||||
name: "ok",
|
||||
endpoint: "/ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []byte(http.StatusText(http.StatusOK)),
|
||||
}
|
||||
|
||||
benchmarkServeStreamHTTPSimple(b, test)
|
||||
}
|
||||
|
||||
func BenchmarkServeStreamHTTPLargeFile(b *testing.B) {
|
||||
test := testRequest{
|
||||
name: "large_file",
|
||||
endpoint: "/large_file",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: testLargeResp,
|
||||
}
|
||||
|
||||
benchmarkServeStreamHTTPSimple(b, test)
|
||||
}
|
||||
+107
-34
@@ -1,6 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
gojson "encoding/json"
|
||||
"fmt"
|
||||
@@ -115,51 +116,53 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var requestErr error
|
||||
switch connType {
|
||||
case TypeControlStream:
|
||||
if err := c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions, c.orchestrator); err != nil {
|
||||
c.controlStreamErr = err
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
requestErr = c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions, c.orchestrator)
|
||||
if requestErr != nil {
|
||||
c.controlStreamErr = requestErr
|
||||
}
|
||||
|
||||
case TypeConfiguration:
|
||||
if err := c.handleConfigurationUpdate(respWriter, r); err != nil {
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
requestErr = c.handleConfigurationUpdate(respWriter, r)
|
||||
|
||||
case TypeWebsocket, TypeHTTP:
|
||||
stripWebsocketUpgradeHeader(r)
|
||||
// Check for tracing on request
|
||||
tr := tracing.NewTracedHTTPRequest(r, c.log)
|
||||
tr := tracing.NewTracedHTTPRequest(r, c.connIndex, c.log)
|
||||
if err := originProxy.ProxyHTTP(respWriter, tr, connType == TypeWebsocket); err != nil {
|
||||
err := fmt.Errorf("Failed to proxy HTTP: %w", err)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
requestErr = fmt.Errorf("Failed to proxy HTTP: %w", err)
|
||||
}
|
||||
|
||||
case TypeTCP:
|
||||
host, err := getRequestHost(r)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(`cloudflared received a warp-routing request with an empty host value: %w`, err)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
requestErr = fmt.Errorf(`cloudflared received a warp-routing request with an empty host value: %w`, err)
|
||||
break
|
||||
}
|
||||
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
|
||||
if err := originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
Dest: host,
|
||||
CFRay: FindCfRayHeader(r),
|
||||
LBProbe: IsLBProbeRequest(r),
|
||||
}); err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
requestErr = originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
Dest: host,
|
||||
CFRay: FindCfRayHeader(r),
|
||||
LBProbe: IsLBProbeRequest(r),
|
||||
CfTraceID: r.Header.Get(tracing.TracerContextName),
|
||||
ConnIndex: c.connIndex,
|
||||
})
|
||||
|
||||
default:
|
||||
err := fmt.Errorf("Received unknown connection type: %s", connType)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
requestErr = fmt.Errorf("Received unknown connection type: %s", connType)
|
||||
}
|
||||
|
||||
if requestErr != nil {
|
||||
c.log.Error().Err(requestErr).Msg("failed to serve incoming request")
|
||||
|
||||
// WriteErrorResponse will return false if status was already written. we need to abort handler.
|
||||
if !respWriter.WriteErrorResponse() {
|
||||
c.log.Debug().Msg("Handler aborted due to failure to write error response after status already sent")
|
||||
panic(http.ErrAbortHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +193,15 @@ func (c *HTTP2Connection) close() {
|
||||
}
|
||||
|
||||
type http2RespWriter struct {
|
||||
r io.Reader
|
||||
w http.ResponseWriter
|
||||
flusher http.Flusher
|
||||
shouldFlush bool
|
||||
log *zerolog.Logger
|
||||
r io.Reader
|
||||
w http.ResponseWriter
|
||||
flusher http.Flusher
|
||||
shouldFlush bool
|
||||
statusWritten bool
|
||||
respHeaders http.Header
|
||||
hijackedMutex sync.Mutex
|
||||
hijackedv bool
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type, log *zerolog.Logger) (*http2RespWriter, error) {
|
||||
@@ -214,15 +221,29 @@ func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type, l
|
||||
w: w,
|
||||
flusher: flusher,
|
||||
shouldFlush: connType.shouldFlush(),
|
||||
respHeaders: make(http.Header),
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) AddTrailer(trailerName, trailerValue string) {
|
||||
if !rp.statusWritten {
|
||||
rp.log.Warn().Msg("Tried to add Trailer to response before status written. Ignoring...")
|
||||
return
|
||||
}
|
||||
|
||||
rp.w.Header().Add(http2.TrailerPrefix+trailerName, trailerValue)
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
if rp.hijacked() {
|
||||
rp.log.Warn().Msg("WriteRespHeaders after hijack")
|
||||
return nil
|
||||
}
|
||||
dest := rp.w.Header()
|
||||
userHeaders := make(http.Header, len(header))
|
||||
for name, values := range header {
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
// lowercase headers for simplicity check
|
||||
h2name := strings.ToLower(name)
|
||||
|
||||
if h2name == "content-length" {
|
||||
@@ -233,7 +254,7 @@ func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) erro
|
||||
|
||||
if h2name == tracing.IntCloudflaredTracingHeader {
|
||||
// Add cf-int-cloudflared-tracing header outside of serialized userHeaders
|
||||
rp.w.Header()[tracing.CanonicalCloudflaredTracingHeader] = values
|
||||
dest[tracing.CanonicalCloudflaredTracingHeader] = values
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -246,24 +267,76 @@ func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) erro
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
dest.Set(CanonicalResponseUserHeaders, SerializeHeaders(userHeaders))
|
||||
|
||||
rp.setResponseMetaHeader(responseMetaHeaderOrigin)
|
||||
// HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1
|
||||
if status == http.StatusSwitchingProtocols {
|
||||
status = http.StatusOK
|
||||
}
|
||||
rp.w.WriteHeader(status)
|
||||
if IsServerSentEvent(header) {
|
||||
if shouldFlush(header) {
|
||||
rp.shouldFlush = true
|
||||
}
|
||||
if rp.shouldFlush {
|
||||
rp.flusher.Flush()
|
||||
}
|
||||
|
||||
rp.statusWritten = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteErrorResponse() {
|
||||
func (rp *http2RespWriter) Header() http.Header {
|
||||
return rp.respHeaders
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteHeader(status int) {
|
||||
if rp.hijacked() {
|
||||
rp.log.Warn().Msg("WriteHeader after hijack")
|
||||
return
|
||||
}
|
||||
rp.WriteRespHeaders(status, rp.respHeaders)
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) hijacked() bool {
|
||||
rp.hijackedMutex.Lock()
|
||||
defer rp.hijackedMutex.Unlock()
|
||||
return rp.hijackedv
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if !rp.statusWritten {
|
||||
return nil, nil, fmt.Errorf("status not yet written before attempting to hijack connection")
|
||||
}
|
||||
// Make sure to flush anything left in the buffer before hijacking
|
||||
if rp.shouldFlush {
|
||||
rp.flusher.Flush()
|
||||
}
|
||||
rp.hijackedMutex.Lock()
|
||||
defer rp.hijackedMutex.Unlock()
|
||||
if rp.hijackedv {
|
||||
return nil, nil, http.ErrHijacked
|
||||
}
|
||||
rp.hijackedv = true
|
||||
conn := &localProxyConnection{rp}
|
||||
// We return the http2RespWriter here because we want to make sure that we flush after every write
|
||||
// otherwise the HTTP2 write buffer waits a few seconds before sending.
|
||||
readWriter := bufio.NewReadWriter(
|
||||
bufio.NewReader(rp),
|
||||
bufio.NewWriter(rp),
|
||||
)
|
||||
return conn, readWriter, nil
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteErrorResponse() bool {
|
||||
if rp.statusWritten {
|
||||
return false
|
||||
}
|
||||
|
||||
rp.setResponseMetaHeader(responseMetaHeaderCfd)
|
||||
rp.w.WriteHeader(http.StatusBadGateway)
|
||||
rp.statusWritten = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) setResponseMetaHeader(value string) {
|
||||
|
||||
+10
-2
@@ -4,12 +4,17 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldConnectionID = "connection"
|
||||
LogFieldLocation = "location"
|
||||
LogFieldIPAddress = "ip"
|
||||
LogFieldProtocol = "protocol"
|
||||
observerChannelBufferSize = 16
|
||||
)
|
||||
|
||||
@@ -41,13 +46,16 @@ func (o *Observer) RegisterSink(sink EventSink) {
|
||||
o.addSinkChan <- sink
|
||||
}
|
||||
|
||||
func (o *Observer) logServerInfo(connIndex uint8, location string, address net.IP, msg string) {
|
||||
func (o *Observer) logConnected(connectionID uuid.UUID, connIndex uint8, location string, address net.IP, protocol Protocol) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
|
||||
o.log.Info().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Str(LogFieldConnectionID, connectionID.String()).
|
||||
Uint8(LogFieldConnIndex, connIndex).
|
||||
Str(LogFieldLocation, location).
|
||||
IPAddr(LogFieldIPAddress, address).
|
||||
Msg(msg)
|
||||
Str(LogFieldProtocol, protocol.String()).
|
||||
Msg("Registered tunnel connection")
|
||||
o.metrics.registerServerLocation(uint8ToString(connIndex), location)
|
||||
}
|
||||
|
||||
|
||||
+78
-108
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
AvailableProtocolFlagMessage = "Available protocols: 'auto' - automatically chooses the best protocol over time (the default; and also the recommended one); 'quic' - based on QUIC, relying on UDP egress to Cloudflare edge; 'http2' - using Go's HTTP2 library, relying on TCP egress to Cloudflare edge; 'h2mux' - Cloudflare's implementation of HTTP/2, deprecated"
|
||||
AvailableProtocolFlagMessage = "Available protocols: 'auto' - automatically chooses the best protocol over time (the default; and also the recommended one); 'quic' - based on QUIC, relying on UDP egress to Cloudflare edge; 'http2' - using Go's HTTP2 library, relying on TCP egress to Cloudflare edge"
|
||||
// edgeH2muxTLSServerName is the server name to establish h2mux connection with edge
|
||||
edgeH2muxTLSServerName = "cftunnel.com"
|
||||
// edgeH2TLSServerName is the server name to establish http2 connection with edge
|
||||
@@ -20,43 +20,32 @@ const (
|
||||
// edgeQUICServerName is the server name to establish quic connection with edge.
|
||||
edgeQUICServerName = "quic.cftunnel.com"
|
||||
AutoSelectFlag = "auto"
|
||||
// SRV and TXT record resolution TTL
|
||||
ResolveTTL = time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
// ProtocolList represents a list of supported protocols for communication with the edge.
|
||||
ProtocolList = []Protocol{H2mux, HTTP2, HTTP2Warp, QUIC, QUICWarp}
|
||||
// ProtocolList represents a list of supported protocols for communication with the edge
|
||||
// in order of precedence for remote percentage fetcher.
|
||||
ProtocolList = []Protocol{QUIC, HTTP2}
|
||||
)
|
||||
|
||||
type Protocol int64
|
||||
|
||||
const (
|
||||
// H2mux protocol can be used both with Classic and Named Tunnels. .
|
||||
H2mux Protocol = iota
|
||||
// HTTP2 is used only with named tunnels. It's more efficient than H2mux for L4 proxying.
|
||||
HTTP2
|
||||
// QUIC is used only with named tunnels.
|
||||
// HTTP2 using golang HTTP2 library for edge connections.
|
||||
HTTP2 Protocol = iota
|
||||
// QUIC using quic-go for edge connections.
|
||||
QUIC
|
||||
// HTTP2Warp is used only with named tunnels. It's useful for warp-routing where we don't want to fallback to
|
||||
// H2mux on HTTP2 failure to connect.
|
||||
HTTP2Warp
|
||||
//QUICWarp is used only with named tunnels. It's useful for warp-routing where we want to fallback to HTTP2 but
|
||||
// don't want HTTP2 to fallback to H2mux
|
||||
QUICWarp
|
||||
)
|
||||
|
||||
// Fallback returns the fallback protocol and whether the protocol has a fallback
|
||||
func (p Protocol) fallback() (Protocol, bool) {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return 0, false
|
||||
case HTTP2:
|
||||
return H2mux, true
|
||||
case HTTP2Warp:
|
||||
return 0, false
|
||||
case QUIC:
|
||||
return HTTP2, true
|
||||
case QUICWarp:
|
||||
return HTTP2Warp, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
@@ -64,11 +53,9 @@ func (p Protocol) fallback() (Protocol, bool) {
|
||||
|
||||
func (p Protocol) String() string {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return "h2mux"
|
||||
case HTTP2, HTTP2Warp:
|
||||
case HTTP2:
|
||||
return "http2"
|
||||
case QUIC, QUICWarp:
|
||||
case QUIC:
|
||||
return "quic"
|
||||
default:
|
||||
return fmt.Sprintf("unknown protocol")
|
||||
@@ -77,15 +64,11 @@ func (p Protocol) String() string {
|
||||
|
||||
func (p Protocol) TLSSettings() *TLSSettings {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeH2muxTLSServerName,
|
||||
}
|
||||
case HTTP2, HTTP2Warp:
|
||||
case HTTP2:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeH2TLSServerName,
|
||||
}
|
||||
case QUIC, QUICWarp:
|
||||
case QUIC:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeQUICServerName,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
@@ -105,6 +88,7 @@ type ProtocolSelector interface {
|
||||
Fallback() (Protocol, bool)
|
||||
}
|
||||
|
||||
// staticProtocolSelector will not provide a different protocol for Fallback
|
||||
type staticProtocolSelector struct {
|
||||
current Protocol
|
||||
}
|
||||
@@ -114,10 +98,11 @@ func (s *staticProtocolSelector) Current() Protocol {
|
||||
}
|
||||
|
||||
func (s *staticProtocolSelector) Fallback() (Protocol, bool) {
|
||||
return 0, false
|
||||
return s.current, false
|
||||
}
|
||||
|
||||
type autoProtocolSelector struct {
|
||||
// remoteProtocolSelector will fetch a list of remote protocols to provide for edge discovery
|
||||
type remoteProtocolSelector struct {
|
||||
lock sync.RWMutex
|
||||
|
||||
current Protocol
|
||||
@@ -126,21 +111,21 @@ type autoProtocolSelector struct {
|
||||
protocolPool []Protocol
|
||||
|
||||
switchThreshold int32
|
||||
fetchFunc PercentageFetcher
|
||||
fetchFunc edgediscovery.PercentageFetcher
|
||||
refreshAfter time.Time
|
||||
ttl time.Duration
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func newAutoProtocolSelector(
|
||||
func newRemoteProtocolSelector(
|
||||
current Protocol,
|
||||
protocolPool []Protocol,
|
||||
switchThreshold int32,
|
||||
fetchFunc PercentageFetcher,
|
||||
fetchFunc edgediscovery.PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) *autoProtocolSelector {
|
||||
return &autoProtocolSelector{
|
||||
) *remoteProtocolSelector {
|
||||
return &remoteProtocolSelector{
|
||||
current: current,
|
||||
protocolPool: protocolPool,
|
||||
switchThreshold: switchThreshold,
|
||||
@@ -151,7 +136,7 @@ func newAutoProtocolSelector(
|
||||
}
|
||||
}
|
||||
|
||||
func (s *autoProtocolSelector) Current() Protocol {
|
||||
func (s *remoteProtocolSelector) Current() Protocol {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if time.Now().Before(s.refreshAfter) {
|
||||
@@ -169,7 +154,13 @@ func (s *autoProtocolSelector) Current() Protocol {
|
||||
return s.current
|
||||
}
|
||||
|
||||
func getProtocol(protocolPool []Protocol, fetchFunc PercentageFetcher, switchThreshold int32) (Protocol, error) {
|
||||
func (s *remoteProtocolSelector) Fallback() (Protocol, bool) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.current.fallback()
|
||||
}
|
||||
|
||||
func getProtocol(protocolPool []Protocol, fetchFunc edgediscovery.PercentageFetcher, switchThreshold int32) (Protocol, error) {
|
||||
protocolPercentages, err := fetchFunc()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -181,99 +172,78 @@ func getProtocol(protocolPool []Protocol, fetchFunc PercentageFetcher, switchThr
|
||||
}
|
||||
}
|
||||
|
||||
return protocolPool[len(protocolPool)-1], nil
|
||||
// Default to first index in protocolPool list
|
||||
return protocolPool[0], nil
|
||||
}
|
||||
|
||||
func (s *autoProtocolSelector) Fallback() (Protocol, bool) {
|
||||
// defaultProtocolSelector will allow for a protocol to have a fallback
|
||||
type defaultProtocolSelector struct {
|
||||
lock sync.RWMutex
|
||||
current Protocol
|
||||
}
|
||||
|
||||
func newDefaultProtocolSelector(
|
||||
current Protocol,
|
||||
) *defaultProtocolSelector {
|
||||
return &defaultProtocolSelector{
|
||||
current: current,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *defaultProtocolSelector) Current() Protocol {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
return s.current
|
||||
}
|
||||
|
||||
func (s *defaultProtocolSelector) Fallback() (Protocol, bool) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.current.fallback()
|
||||
}
|
||||
|
||||
type PercentageFetcher func() (edgediscovery.ProtocolPercents, error)
|
||||
|
||||
func NewProtocolSelector(
|
||||
protocolFlag string,
|
||||
warpRoutingEnabled bool,
|
||||
namedTunnel *NamedTunnelProperties,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
accountTag string,
|
||||
tunnelTokenProvided bool,
|
||||
needPQ bool,
|
||||
protocolFetcher edgediscovery.PercentageFetcher,
|
||||
resolveTTL time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) (ProtocolSelector, error) {
|
||||
// Classic tunnel is only supported with h2mux
|
||||
if namedTunnel == nil {
|
||||
// With --post-quantum, we force quic
|
||||
if needPQ {
|
||||
return &staticProtocolSelector{
|
||||
current: H2mux,
|
||||
current: QUIC,
|
||||
}, nil
|
||||
}
|
||||
|
||||
threshold := switchThreshold(namedTunnel.Credentials.AccountTag)
|
||||
fetchedProtocol, err := getProtocol([]Protocol{QUIC, HTTP2}, fetchFunc, threshold)
|
||||
threshold := switchThreshold(accountTag)
|
||||
fetchedProtocol, err := getProtocol(ProtocolList, protocolFetcher, threshold)
|
||||
log.Debug().Msgf("Fetched protocol: %s", fetchedProtocol)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Unable to lookup protocol. Defaulting to `http2`. If this fails, you can set `--protocol h2mux` in your cloudflared command.")
|
||||
return &staticProtocolSelector{
|
||||
current: HTTP2,
|
||||
}, nil
|
||||
}
|
||||
if warpRoutingEnabled {
|
||||
if protocolFlag == H2mux.String() || fetchedProtocol == H2mux {
|
||||
log.Warn().Msg("Warp routing is not supported in h2mux protocol. Upgrading to http2 to allow it.")
|
||||
protocolFlag = HTTP2.String()
|
||||
fetchedProtocol = HTTP2Warp
|
||||
}
|
||||
return selectWarpRoutingProtocols(protocolFlag, fetchFunc, ttl, log, threshold, fetchedProtocol)
|
||||
log.Warn().Msg("Unable to lookup protocol percentage.")
|
||||
// Falling through here since 'auto' is handled in the switch and failing
|
||||
// to do the protocol lookup isn't a failure since it can be triggered again
|
||||
// after the TTL.
|
||||
}
|
||||
|
||||
return selectNamedTunnelProtocols(protocolFlag, fetchFunc, ttl, log, threshold, fetchedProtocol)
|
||||
}
|
||||
|
||||
func selectNamedTunnelProtocols(
|
||||
protocolFlag string,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
threshold int32,
|
||||
protocol Protocol,
|
||||
) (ProtocolSelector, error) {
|
||||
// If the user picks a protocol, then we stick to it no matter what.
|
||||
switch protocolFlag {
|
||||
case H2mux.String():
|
||||
return &staticProtocolSelector{current: H2mux}, nil
|
||||
case "h2mux":
|
||||
// Any users still requesting h2mux will be upgraded to http2 instead
|
||||
log.Warn().Msg("h2mux is no longer a supported protocol: upgrading edge connection to http2. Please remove '--protocol h2mux' from runtime arguments to remove this warning.")
|
||||
return &staticProtocolSelector{current: HTTP2}, nil
|
||||
case QUIC.String():
|
||||
return &staticProtocolSelector{current: QUIC}, nil
|
||||
case HTTP2.String():
|
||||
return &staticProtocolSelector{current: HTTP2}, nil
|
||||
}
|
||||
|
||||
// If the user does not pick (hopefully the majority) then we use the one derived from the TXT DNS record and
|
||||
// fallback on failures.
|
||||
if protocolFlag == AutoSelectFlag {
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUIC, HTTP2, H2mux}, threshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
|
||||
func selectWarpRoutingProtocols(
|
||||
protocolFlag string,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
threshold int32,
|
||||
protocol Protocol,
|
||||
) (ProtocolSelector, error) {
|
||||
// If the user picks a protocol, then we stick to it no matter what.
|
||||
switch protocolFlag {
|
||||
case QUIC.String():
|
||||
return &staticProtocolSelector{current: QUICWarp}, nil
|
||||
case HTTP2.String():
|
||||
return &staticProtocolSelector{current: HTTP2Warp}, nil
|
||||
}
|
||||
|
||||
// If the user does not pick (hopefully the majority) then we use the one derived from the TXT DNS record and
|
||||
// fallback on failures.
|
||||
if protocolFlag == AutoSelectFlag {
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUICWarp, HTTP2Warp}, threshold, fetchFunc, ttl, log), nil
|
||||
case AutoSelectFlag:
|
||||
// When a --token is provided, we want to start with QUIC but have fallback to HTTP2
|
||||
if tunnelTokenProvided {
|
||||
return newDefaultProtocolSelector(QUIC), nil
|
||||
}
|
||||
return newRemoteProtocolSelector(fetchedProtocol, ProtocolList, threshold, protocolFetcher, resolveTTL, log), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
|
||||
+47
-180
@@ -3,7 +3,6 @@ package connection
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@@ -11,19 +10,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
testNoTTL = 0
|
||||
noWarpRoutingEnabled = false
|
||||
testNoTTL = 0
|
||||
testAccountTag = "testAccountTag"
|
||||
)
|
||||
|
||||
var (
|
||||
testNamedTunnelProperties = &NamedTunnelProperties{
|
||||
Credentials: Credentials{
|
||||
AccountTag: "testAccountTag",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) PercentageFetcher {
|
||||
func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) edgediscovery.PercentageFetcher {
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
if getError {
|
||||
return nil, fmt.Errorf("failed to fetch percentage")
|
||||
@@ -37,7 +28,7 @@ type dynamicMockFetcher struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (dmf *dynamicMockFetcher) fetch() PercentageFetcher {
|
||||
func (dmf *dynamicMockFetcher) fetch() edgediscovery.PercentageFetcher {
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
return dmf.protocolPercents, dmf.err
|
||||
}
|
||||
@@ -45,181 +36,58 @@ func (dmf *dynamicMockFetcher) fetch() PercentageFetcher {
|
||||
|
||||
func TestNewProtocolSelector(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
expectedProtocol Protocol
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
warpRoutingEnabled bool
|
||||
namedTunnelConfig *NamedTunnelProperties
|
||||
fetchFunc PercentageFetcher
|
||||
wantErr bool
|
||||
name string
|
||||
protocol string
|
||||
tunnelTokenProvided bool
|
||||
needPQ bool
|
||||
expectedProtocol Protocol
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "classic tunnel",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: H2mux,
|
||||
namedTunnelConfig: nil,
|
||||
name: "named tunnel with unknown protocol",
|
||||
protocol: "unknown",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "named tunnel over h2mux",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: func() (edgediscovery.ProtocolPercents, error) { return nil, nil },
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel over http2",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel http2 disabled still gets http2 because it is manually picked",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel quic disabled still gets quic because it is manually picked",
|
||||
protocol: "quic",
|
||||
expectedProtocol: QUIC,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel quic and http2 disabled",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel quic disabled",
|
||||
protocol: AutoSelectFlag,
|
||||
name: "named tunnel with h2mux: force to http2",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: HTTP2,
|
||||
// Hasfallback true is because if http2 fails, then we further fallback to h2mux.
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto all http2 disabled",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
name: "named tunnel with http2: no fallback",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to h2mux",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
name: "named tunnel with auto: quic",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: QUIC,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to http2",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
name: "named tunnel (post quantum)",
|
||||
protocol: AutoSelectFlag,
|
||||
needPQ: true,
|
||||
expectedProtocol: QUIC,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to quic",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: QUIC,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing requesting h2mux",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing requesting h2mux picks HTTP2 even if http2 percent is -1",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing http2",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing quic",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: QUICWarp,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2Warp,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing auto",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing auto- quic",
|
||||
protocol: AutoSelectFlag,
|
||||
expectedProtocol: QUICWarp,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2Warp,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
// None named tunnel can only use h2mux, so specifying an unknown protocol is not an error
|
||||
name: "classic tunnel unknown protocol",
|
||||
protocol: "unknown",
|
||||
expectedProtocol: H2mux,
|
||||
},
|
||||
{
|
||||
name: "named tunnel unknown protocol",
|
||||
protocol: "unknown",
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "named tunnel fetch error",
|
||||
protocol: AutoSelectFlag,
|
||||
fetchFunc: mockFetcher(true),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
expectedProtocol: HTTP2,
|
||||
wantErr: false,
|
||||
name: "named tunnel (post quantum) w/http2",
|
||||
protocol: "http2",
|
||||
needPQ: true,
|
||||
expectedProtocol: QUIC,
|
||||
},
|
||||
}
|
||||
|
||||
fetcher := dynamicMockFetcher{
|
||||
protocolPercents: edgediscovery.ProtocolPercents{},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
selector, err := NewProtocolSelector(test.protocol, test.warpRoutingEnabled, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector(test.protocol, testAccountTag, test.tunnelTokenProvided, test.needPQ, fetcher.fetch(), ResolveTTL, &log)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, fmt.Sprintf("test %s failed", test.name))
|
||||
} else {
|
||||
@@ -237,15 +105,15 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
|
||||
func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, noWarpRoutingEnabled, testNamedTunnelProperties, fetcher.fetch(), testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}}
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
@@ -255,10 +123,10 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}}
|
||||
fetcher.err = nil
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}}
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}}
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
@@ -267,7 +135,7 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
// Since the user chooses http2 on purpose, we always stick to it.
|
||||
selector, err := NewProtocolSelector("http2", noWarpRoutingEnabled, testNamedTunnelProperties, fetcher.fetch(), testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
@@ -294,13 +162,12 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
}
|
||||
|
||||
func TestProtocolSelectorRefreshTTL(t *testing.T) {
|
||||
func TestAutoProtocolSelectorNoRefreshWithToken(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}}
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, noWarpRoutingEnabled, testNamedTunnelProperties, fetcher.fetch(), time.Hour, &log)
|
||||
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, false, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 0}}
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
}
|
||||
|
||||
+263
-39
@@ -1,24 +1,32 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/datagramsession"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
@@ -37,6 +45,11 @@ const (
|
||||
demuxChanCapacity = 16
|
||||
)
|
||||
|
||||
var (
|
||||
portForConnIndex = make(map[uint8]int, 0)
|
||||
portMapMutex sync.Mutex
|
||||
)
|
||||
|
||||
// QUICConnection represents the type that facilitates Proxying via QUIC streams.
|
||||
type QUICConnection struct {
|
||||
session quic.Connection
|
||||
@@ -45,29 +58,48 @@ type QUICConnection struct {
|
||||
// sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer
|
||||
sessionManager datagramsession.Manager
|
||||
// datagramMuxer mux/demux datagrams from quic connection
|
||||
datagramMuxer *quicpogs.DatagramMuxer
|
||||
datagramMuxer *quicpogs.DatagramMuxerV2
|
||||
packetRouter *ingress.PacketRouter
|
||||
controlStreamHandler ControlStreamHandler
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
connIndex uint8
|
||||
}
|
||||
|
||||
// NewQUICConnection returns a new instance of QUICConnection.
|
||||
func NewQUICConnection(
|
||||
quicConfig *quic.Config,
|
||||
edgeAddr net.Addr,
|
||||
localAddr net.IP,
|
||||
connIndex uint8,
|
||||
tlsConfig *tls.Config,
|
||||
orchestrator Orchestrator,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler ControlStreamHandler,
|
||||
logger *zerolog.Logger,
|
||||
packetRouterConfig *ingress.GlobalRouterConfig,
|
||||
) (*QUICConnection, error) {
|
||||
session, err := quic.DialAddr(edgeAddr.String(), tlsConfig, quicConfig)
|
||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := quic.Dial(udpConn, edgeAddr, edgeAddr.String(), tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
// close the udp server socket in case of error connecting to the edge
|
||||
udpConn.Close()
|
||||
return nil, &EdgeQuicDialError{Cause: err}
|
||||
}
|
||||
|
||||
demuxChan := make(chan *quicpogs.SessionDatagram, demuxChanCapacity)
|
||||
datagramMuxer := quicpogs.NewDatagramMuxer(session, logger, demuxChan)
|
||||
sessionManager := datagramsession.NewManager(logger, datagramMuxer.MuxSession, demuxChan)
|
||||
// wrap the session, so that the UDPConn is closed after session is closed.
|
||||
session = &wrapCloseableConnQuicConnection{
|
||||
session,
|
||||
udpConn,
|
||||
}
|
||||
|
||||
sessionDemuxChan := make(chan *packet.Session, demuxChanCapacity)
|
||||
datagramMuxer := quicpogs.NewDatagramMuxerV2(session, logger, sessionDemuxChan)
|
||||
sessionManager := datagramsession.NewManager(logger, datagramMuxer.SendToSession, sessionDemuxChan)
|
||||
packetRouter := ingress.NewPacketRouter(packetRouterConfig, datagramMuxer, logger, orchestrator.WarpRoutingEnabled)
|
||||
|
||||
return &QUICConnection{
|
||||
session: session,
|
||||
@@ -75,8 +107,10 @@ func NewQUICConnection(
|
||||
logger: logger,
|
||||
sessionManager: sessionManager,
|
||||
datagramMuxer: datagramMuxer,
|
||||
packetRouter: packetRouter,
|
||||
controlStreamHandler: controlStreamHandler,
|
||||
connOptions: connOptions,
|
||||
connIndex: connIndex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -110,11 +144,14 @@ func (q *QUICConnection) Serve(ctx context.Context) error {
|
||||
defer cancel()
|
||||
return q.sessionManager.Serve(ctx)
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return q.datagramMuxer.ServeReceive(ctx)
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return q.packetRouter.Serve(ctx)
|
||||
})
|
||||
|
||||
return errGroup.Wait()
|
||||
}
|
||||
@@ -156,11 +193,16 @@ func (q *QUICConnection) runStream(quicStream quic.Stream) {
|
||||
defer stream.Close()
|
||||
|
||||
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
|
||||
// code executed in the code path of handleStream don't trigger an earlier close to the downstream stream.
|
||||
// So, we wrap the stream with a no-op closer and only this method can actually close the stream.
|
||||
noCloseStream := &nopCloserReadWriter{stream}
|
||||
// code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream.
|
||||
// So, we wrap the stream with a no-op write closer and only this method can actually close write side of the stream.
|
||||
// A call to close will simulate a close to the read-side, which will fail subsequent reads.
|
||||
noCloseStream := &nopCloserReadWriter{ReadWriteCloser: stream}
|
||||
if err := q.handleStream(ctx, noCloseStream); err != nil {
|
||||
q.logger.Err(err).Msg("Failed to handle QUIC stream")
|
||||
q.logger.Debug().Err(err).Msg("Failed to handle QUIC stream")
|
||||
|
||||
// if we received an error at this level, then close write side of stream with an error, which will result in
|
||||
// RST_STREAM frame.
|
||||
quicStream.CancelWrite(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,64 +235,105 @@ func (q *QUICConnection) handleDataStream(ctx context.Context, stream *quicpogs.
|
||||
return err
|
||||
}
|
||||
|
||||
if err := q.dispatchRequest(ctx, stream, err, request); err != nil {
|
||||
_ = stream.WriteConnectResponseData(err)
|
||||
if err, connectResponseSent := q.dispatchRequest(ctx, stream, err, request); err != nil {
|
||||
q.logger.Err(err).Str("type", request.Type.String()).Str("dest", request.Dest).Msg("Request failed")
|
||||
|
||||
// if the connectResponse was already sent and we had an error, we need to propagate it up, so that the stream is
|
||||
// closed with an RST_STREAM frame
|
||||
if connectResponseSent {
|
||||
return err
|
||||
}
|
||||
|
||||
if writeRespErr := stream.WriteConnectResponseData(err); writeRespErr != nil {
|
||||
return writeRespErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QUICConnection) dispatchRequest(ctx context.Context, stream *quicpogs.RequestServerStream, err error, request *quicpogs.ConnectRequest) error {
|
||||
// dispatchRequest will dispatch the request depending on the type and returns an error if it occurs.
|
||||
// More importantly, it also tells if the during processing of the request the ConnectResponse metadata was sent downstream.
|
||||
// This is important since it informs
|
||||
func (q *QUICConnection) dispatchRequest(ctx context.Context, stream *quicpogs.RequestServerStream, err error, request *quicpogs.ConnectRequest) (error, bool) {
|
||||
originProxy, err := q.orchestrator.GetOriginProxy()
|
||||
if err != nil {
|
||||
return err
|
||||
return err, false
|
||||
}
|
||||
|
||||
switch request.Type {
|
||||
case quicpogs.ConnectionTypeHTTP, quicpogs.ConnectionTypeWebsocket:
|
||||
tracedReq, err := buildHTTPRequest(ctx, request, stream, q.logger)
|
||||
tracedReq, err := buildHTTPRequest(ctx, request, stream, q.connIndex, q.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
return err, false
|
||||
}
|
||||
w := newHTTPResponseAdapter(stream)
|
||||
return originProxy.ProxyHTTP(w, tracedReq, request.Type == quicpogs.ConnectionTypeWebsocket)
|
||||
return originProxy.ProxyHTTP(&w, tracedReq, request.Type == quicpogs.ConnectionTypeWebsocket), w.connectResponseSent
|
||||
|
||||
case quicpogs.ConnectionTypeTCP:
|
||||
rwa := &streamReadWriteAcker{stream}
|
||||
rwa := &streamReadWriteAcker{RequestServerStream: stream}
|
||||
metadata := request.MetadataMap()
|
||||
return originProxy.ProxyTCP(ctx, rwa, &TCPRequest{
|
||||
Dest: request.Dest,
|
||||
FlowID: metadata[QUICMetadataFlowID],
|
||||
CfTraceID: metadata[tracing.TracerContextName],
|
||||
})
|
||||
ConnIndex: q.connIndex,
|
||||
}), rwa.connectResponseSent
|
||||
default:
|
||||
return errors.Errorf("unsupported error type: %s", request.Type), false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) error {
|
||||
return rpcStream.Serve(q, q, q.logger)
|
||||
if err := rpcStream.Serve(q, q, q.logger); err != nil {
|
||||
q.logger.Err(err).Msg("failed handling RPC stream")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterUdpSession is the RPC method invoked by edge to register and run a session
|
||||
func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error {
|
||||
func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*tunnelpogs.RegisterUdpSessionResponse, error) {
|
||||
traceCtx := tracing.NewTracedContext(ctx, traceContext, q.logger)
|
||||
ctx, registerSpan := traceCtx.Tracer().Start(traceCtx, "register-session", trace.WithAttributes(
|
||||
attribute.String("session-id", sessionID.String()),
|
||||
attribute.String("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)),
|
||||
))
|
||||
log := q.logger.With().Int(management.EventTypeKey, int(management.UDP)).Logger()
|
||||
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
|
||||
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
|
||||
originProxy, err := ingress.DialUDP(dstIP, dstPort)
|
||||
if err != nil {
|
||||
q.logger.Err(err).Msgf("Failed to create udp proxy to %s:%d", dstIP, dstPort)
|
||||
return err
|
||||
log.Err(err).Msgf("Failed to create udp proxy to %s:%d", dstIP, dstPort)
|
||||
tracing.EndWithErrorStatus(registerSpan, err)
|
||||
return nil, err
|
||||
}
|
||||
registerSpan.SetAttributes(
|
||||
attribute.Bool("socket-bind-success", true),
|
||||
attribute.String("src", originProxy.LocalAddr().String()),
|
||||
)
|
||||
|
||||
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
|
||||
if err != nil {
|
||||
q.logger.Err(err).Str("sessionID", sessionID.String()).Msgf("Failed to register udp session")
|
||||
return err
|
||||
log.Err(err).Str("sessionID", sessionID.String()).Msgf("Failed to register udp session")
|
||||
tracing.EndWithErrorStatus(registerSpan, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go q.serveUDPSession(session, closeAfterIdleHint)
|
||||
|
||||
q.logger.Debug().Str("sessionID", sessionID.String()).Str("src", originProxy.LocalAddr().String()).Str("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)).Msgf("Registered session")
|
||||
return nil
|
||||
log.Debug().
|
||||
Str("sessionID", sessionID.String()).
|
||||
Str("src", originProxy.LocalAddr().String()).
|
||||
Str("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)).
|
||||
Msgf("Registered session")
|
||||
tracing.End(registerSpan)
|
||||
|
||||
resp := tunnelpogs.RegisterUdpSessionResponse{
|
||||
Spans: traceCtx.GetProtoSpans(),
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, closeAfterIdleHint time.Duration) {
|
||||
@@ -264,7 +347,10 @@ func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, close
|
||||
q.closeUDPSession(ctx, session.ID, "terminated without error")
|
||||
}
|
||||
}
|
||||
q.logger.Debug().Err(err).Str("sessionID", session.ID.String()).Msg("Session terminated")
|
||||
q.logger.Debug().Err(err).
|
||||
Int(management.EventTypeKey, int(management.UDP)).
|
||||
Str("sessionID", session.ID.String()).
|
||||
Msg("Session terminated")
|
||||
}
|
||||
|
||||
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
|
||||
@@ -274,7 +360,9 @@ func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUI
|
||||
if err != nil {
|
||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||
// with edge
|
||||
q.logger.Debug().Err(err).Str("sessionID", sessionID.String()).
|
||||
q.logger.Debug().Err(err).
|
||||
Int(management.EventTypeKey, int(management.UDP)).
|
||||
Str("sessionID", sessionID.String()).
|
||||
Msgf("Failed to open quic stream to unregister udp session with edge")
|
||||
return
|
||||
}
|
||||
@@ -306,27 +394,39 @@ func (q *QUICConnection) UpdateConfiguration(ctx context.Context, version int32,
|
||||
// the client.
|
||||
type streamReadWriteAcker struct {
|
||||
*quicpogs.RequestServerStream
|
||||
connectResponseSent bool
|
||||
}
|
||||
|
||||
// AckConnection acks response back to the proxy.
|
||||
func (s *streamReadWriteAcker) AckConnection(tracePropagation string) error {
|
||||
metadata := quicpogs.Metadata{
|
||||
Key: tracing.CanonicalCloudflaredTracingHeader,
|
||||
Val: tracePropagation,
|
||||
metadata := []quicpogs.Metadata{}
|
||||
// Only add tracing if provided by origintunneld
|
||||
if tracePropagation != "" {
|
||||
metadata = append(metadata, quicpogs.Metadata{
|
||||
Key: tracing.CanonicalCloudflaredTracingHeader,
|
||||
Val: tracePropagation,
|
||||
})
|
||||
}
|
||||
return s.WriteConnectResponseData(nil, metadata)
|
||||
s.connectResponseSent = true
|
||||
return s.WriteConnectResponseData(nil, metadata...)
|
||||
}
|
||||
|
||||
// httpResponseAdapter translates responses written by the HTTP Proxy into ones that can be used in QUIC.
|
||||
type httpResponseAdapter struct {
|
||||
*quicpogs.RequestServerStream
|
||||
headers http.Header
|
||||
connectResponseSent bool
|
||||
}
|
||||
|
||||
func newHTTPResponseAdapter(s *quicpogs.RequestServerStream) httpResponseAdapter {
|
||||
return httpResponseAdapter{s}
|
||||
return httpResponseAdapter{RequestServerStream: s, headers: make(http.Header)}
|
||||
}
|
||||
|
||||
func (hrw httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
|
||||
func (hrw *httpResponseAdapter) AddTrailer(trailerName, trailerValue string) {
|
||||
// we do not support trailers over QUIC
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
|
||||
metadata := make([]quicpogs.Metadata, 0)
|
||||
metadata = append(metadata, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(status)})
|
||||
for k, vv := range header {
|
||||
@@ -335,17 +435,41 @@ func (hrw httpResponseAdapter) WriteRespHeaders(status int, header http.Header)
|
||||
metadata = append(metadata, quicpogs.Metadata{Key: httpHeaderKey, Val: v})
|
||||
}
|
||||
}
|
||||
|
||||
return hrw.WriteConnectResponseData(nil, metadata...)
|
||||
}
|
||||
|
||||
func (hrw httpResponseAdapter) WriteErrorResponse(err error) {
|
||||
func (hrw *httpResponseAdapter) Header() http.Header {
|
||||
return hrw.headers
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteHeader(status int) {
|
||||
hrw.WriteRespHeaders(status, hrw.headers)
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
conn := &localProxyConnection{hrw.ReadWriteCloser}
|
||||
readWriter := bufio.NewReadWriter(
|
||||
bufio.NewReader(hrw.ReadWriteCloser),
|
||||
bufio.NewWriter(hrw.ReadWriteCloser),
|
||||
)
|
||||
return conn, readWriter, nil
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteErrorResponse(err error) {
|
||||
hrw.WriteConnectResponseData(err, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
|
||||
}
|
||||
|
||||
func (hrw *httpResponseAdapter) WriteConnectResponseData(respErr error, metadata ...quicpogs.Metadata) error {
|
||||
hrw.connectResponseSent = true
|
||||
return hrw.RequestServerStream.WriteConnectResponseData(respErr, metadata...)
|
||||
}
|
||||
|
||||
func buildHTTPRequest(
|
||||
ctx context.Context,
|
||||
connectRequest *quicpogs.ConnectRequest,
|
||||
body io.ReadCloser,
|
||||
connIndex uint8,
|
||||
log *zerolog.Logger,
|
||||
) (*tracing.TracedHTTPRequest, error) {
|
||||
metadata := connectRequest.MetadataMap()
|
||||
@@ -389,7 +513,7 @@ func buildHTTPRequest(
|
||||
stripWebsocketUpgradeHeader(req)
|
||||
|
||||
// Check for tracing on request
|
||||
tracedReq := tracing.NewTracedHTTPRequest(req, log)
|
||||
tracedReq := tracing.NewTracedHTTPRequest(req, connIndex, log)
|
||||
return tracedReq, err
|
||||
}
|
||||
|
||||
@@ -408,10 +532,110 @@ func isTransferEncodingChunked(req *http.Request) bool {
|
||||
return strings.Contains(strings.ToLower(transferEncodingVal), "chunked")
|
||||
}
|
||||
|
||||
// A helper struct that guarantees a call to close only affects read side, but not write side.
|
||||
type nopCloserReadWriter struct {
|
||||
io.ReadWriteCloser
|
||||
|
||||
// for use by Read only
|
||||
// we don't need a memory barrier here because there is an implicit assumption that
|
||||
// Read calls can't happen concurrently by different go-routines.
|
||||
sawEOF bool
|
||||
// should be updated and read using atomic primitives.
|
||||
// value is read in Read method and written in Close method, which could be done by different
|
||||
// go-routines.
|
||||
closed uint32
|
||||
}
|
||||
|
||||
func (n *nopCloserReadWriter) Close() error {
|
||||
func (np *nopCloserReadWriter) Read(p []byte) (n int, err error) {
|
||||
if np.sawEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if atomic.LoadUint32(&np.closed) > 0 {
|
||||
return 0, fmt.Errorf("closed by handler")
|
||||
}
|
||||
|
||||
n, err = np.ReadWriteCloser.Read(p)
|
||||
if err == io.EOF {
|
||||
np.sawEOF = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (np *nopCloserReadWriter) Close() error {
|
||||
atomic.StoreUint32(&np.closed, 1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// muxerWrapper wraps DatagramMuxerV2 to satisfy the packet.FunnelUniPipe interface
|
||||
type muxerWrapper struct {
|
||||
muxer *quicpogs.DatagramMuxerV2
|
||||
}
|
||||
|
||||
func (rp *muxerWrapper) SendPacket(dst netip.Addr, pk packet.RawPacket) error {
|
||||
return rp.muxer.SendPacket(quicpogs.RawPacket(pk))
|
||||
}
|
||||
|
||||
func (rp *muxerWrapper) ReceivePacket(ctx context.Context) (packet.RawPacket, error) {
|
||||
pk, err := rp.muxer.ReceivePacket(ctx)
|
||||
if err != nil {
|
||||
return packet.RawPacket{}, err
|
||||
}
|
||||
rawPacket, ok := pk.(quicpogs.RawPacket)
|
||||
if ok {
|
||||
return packet.RawPacket(rawPacket), nil
|
||||
}
|
||||
return packet.RawPacket{}, fmt.Errorf("unexpected packet type %+v", pk)
|
||||
}
|
||||
|
||||
func (rp *muxerWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, logger *zerolog.Logger) (*net.UDPConn, error) {
|
||||
portMapMutex.Lock()
|
||||
defer portMapMutex.Unlock()
|
||||
|
||||
if localIP == nil {
|
||||
localIP = net.IPv4zero
|
||||
}
|
||||
|
||||
// if port was not set yet, it will be zero, so bind will randomly allocate one.
|
||||
if port, ok := portForConnIndex[connIndex]; ok {
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP, Port: port})
|
||||
// if there wasn't an error, or if port was 0 (independently of error or not, just return)
|
||||
if err == nil {
|
||||
return udpConn, nil
|
||||
} else {
|
||||
logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// if we reached here, then there was an error or port as not been allocated it.
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP, Port: 0})
|
||||
if err == nil {
|
||||
udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to cast to udpConn")
|
||||
}
|
||||
portForConnIndex[connIndex] = udpAddr.Port
|
||||
} else {
|
||||
delete(portForConnIndex, connIndex)
|
||||
}
|
||||
|
||||
return udpConn, err
|
||||
}
|
||||
|
||||
type wrapCloseableConnQuicConnection struct {
|
||||
quic.Connection
|
||||
udpConn *net.UDPConn
|
||||
}
|
||||
|
||||
func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error {
|
||||
err := w.Connection.CloseWithError(errorCode, reason)
|
||||
w.udpConn.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+120
-27
@@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/datagramsession"
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
@@ -140,24 +141,33 @@ func TestQUICServer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
for i, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
quicListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
serverDone := make(chan struct{})
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
quicServer(
|
||||
t, udpListener, testTLSServerConfig, testQUICConfig,
|
||||
test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse,
|
||||
ctx, t, quicListener, test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse,
|
||||
)
|
||||
close(serverDone)
|
||||
}()
|
||||
|
||||
qc := testQUICConnection(udpListener.LocalAddr(), t)
|
||||
go qc.Serve(ctx)
|
||||
qc := testQUICConnection(udpListener.LocalAddr(), t, uint8(i))
|
||||
|
||||
wg.Wait()
|
||||
connDone := make(chan struct{})
|
||||
go func() {
|
||||
qc.Serve(ctx)
|
||||
close(connDone)
|
||||
}()
|
||||
|
||||
<-serverDone
|
||||
cancel()
|
||||
<-connDone
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -175,23 +185,16 @@ func (fakeControlStream) IsStopped() bool {
|
||||
}
|
||||
|
||||
func quicServer(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
conn net.PacketConn,
|
||||
tlsConf *tls.Config,
|
||||
config *quic.Config,
|
||||
listener quic.Listener,
|
||||
dest string,
|
||||
connectionType quicpogs.ConnectionType,
|
||||
metadata []quicpogs.Metadata,
|
||||
message []byte,
|
||||
expectedResponse []byte,
|
||||
) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
earlyListener, err := quic.Listen(conn, tlsConf, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
session, err := earlyListener.Accept(ctx)
|
||||
session, err := listener.Accept(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
quicStream, err := session.OpenStreamSync(context.Background())
|
||||
@@ -480,8 +483,9 @@ func TestBuildHTTPRequest(t *testing.T) {
|
||||
|
||||
log := zerolog.Nop()
|
||||
for _, test := range tests {
|
||||
test := test // capture range variable
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req, err := buildHTTPRequest(context.Background(), test.connectRequest, test.body, &log)
|
||||
req, err := buildHTTPRequest(context.Background(), test.connectRequest, test.body, 0, &log)
|
||||
assert.NoError(t, err)
|
||||
test.req = test.req.WithContext(req.Context())
|
||||
assert.Equal(t, test.req, req.Request)
|
||||
@@ -517,7 +521,8 @@ func TestServeUDPSession(t *testing.T) {
|
||||
edgeQUICSessionChan <- edgeQUICSession
|
||||
}()
|
||||
|
||||
qc := testQUICConnection(val, t)
|
||||
// Random index to avoid reusing port
|
||||
qc := testQUICConnection(val, t, 28)
|
||||
go qc.Serve(ctx)
|
||||
|
||||
edgeQUICSession := <-edgeQUICSessionChan
|
||||
@@ -527,6 +532,75 @@ func TestServeUDPSession(t *testing.T) {
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestNopCloserReadWriterCloseBeforeEOF(t *testing.T) {
|
||||
readerWriter := nopCloserReadWriter{ReadWriteCloser: &mockReaderNoopWriter{Reader: strings.NewReader("123456789")}}
|
||||
buffer := make([]byte, 5)
|
||||
|
||||
n, err := readerWriter.Read(buffer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 5)
|
||||
|
||||
// close
|
||||
require.NoError(t, readerWriter.Close())
|
||||
|
||||
// read should get error
|
||||
n, err = readerWriter.Read(buffer)
|
||||
require.Equal(t, n, 0)
|
||||
require.Equal(t, err, fmt.Errorf("closed by handler"))
|
||||
}
|
||||
|
||||
func TestNopCloserReadWriterCloseAfterEOF(t *testing.T) {
|
||||
readerWriter := nopCloserReadWriter{ReadWriteCloser: &mockReaderNoopWriter{Reader: strings.NewReader("123456789")}}
|
||||
buffer := make([]byte, 20)
|
||||
|
||||
n, err := readerWriter.Read(buffer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, n, 9)
|
||||
|
||||
// force another read to read eof
|
||||
_, err = readerWriter.Read(buffer)
|
||||
require.Equal(t, err, io.EOF)
|
||||
|
||||
// close
|
||||
require.NoError(t, readerWriter.Close())
|
||||
|
||||
// read should get EOF still
|
||||
n, err = readerWriter.Read(buffer)
|
||||
require.Equal(t, n, 0)
|
||||
require.Equal(t, err, io.EOF)
|
||||
}
|
||||
|
||||
func TestCreateUDPConnReuseSourcePort(t *testing.T) {
|
||||
logger := zerolog.Nop()
|
||||
conn, err := createUDPConnForConnIndex(0, nil, &logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
getPortFunc := func(conn *net.UDPConn) int {
|
||||
addr := conn.LocalAddr().(*net.UDPAddr)
|
||||
return addr.Port
|
||||
}
|
||||
|
||||
initialPort := getPortFunc(conn)
|
||||
|
||||
// close conn
|
||||
conn.Close()
|
||||
|
||||
// should get the same port as before.
|
||||
conn, err = createUDPConnForConnIndex(0, nil, &logger)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, initialPort, getPortFunc(conn))
|
||||
|
||||
// new index, should get a different port
|
||||
conn1, err := createUDPConnForConnIndex(1, nil, &logger)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, initialPort, getPortFunc(conn1))
|
||||
|
||||
// not closing the conn and trying to obtain a new conn for same index should give a different random port
|
||||
conn, err = createUDPConnForConnIndex(0, nil, &logger)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, initialPort, getPortFunc(conn))
|
||||
}
|
||||
|
||||
func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) {
|
||||
var (
|
||||
payload = []byte(t.Name())
|
||||
@@ -543,8 +617,12 @@ func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.
|
||||
close(sessionDone)
|
||||
}()
|
||||
|
||||
// Send a message to the quic session on edge side, it should be deumx to this datagram session
|
||||
muxedPayload := append(payload, sessionID[:]...)
|
||||
// Send a message to the quic session on edge side, it should be deumx to this datagram v2 session
|
||||
muxedPayload, err := quicpogs.SuffixSessionID(sessionID, payload)
|
||||
require.NoError(t, err)
|
||||
muxedPayload, err = quicpogs.SuffixType(muxedPayload, quicpogs.DatagramTypeUDP)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = edgeQUICSession.SendMessage(muxedPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -613,8 +691,8 @@ type mockSessionRPCServer struct {
|
||||
calledUnregisterChan chan struct{}
|
||||
}
|
||||
|
||||
func (s mockSessionRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error {
|
||||
return fmt.Errorf("mockSessionRPCServer doesn't implement RegisterUdpSession")
|
||||
func (s mockSessionRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) {
|
||||
return nil, fmt.Errorf("mockSessionRPCServer doesn't implement RegisterUdpSession")
|
||||
}
|
||||
|
||||
func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, reason string) error {
|
||||
@@ -628,7 +706,7 @@ func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionI
|
||||
return nil
|
||||
}
|
||||
|
||||
func testQUICConnection(udpListenerAddr net.Addr, t *testing.T) *QUICConnection {
|
||||
func testQUICConnection(udpListenerAddr net.Addr, t *testing.T, index uint8) *QUICConnection {
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
@@ -638,12 +716,27 @@ func testQUICConnection(udpListenerAddr net.Addr, t *testing.T) *QUICConnection
|
||||
qc, err := NewQUICConnection(
|
||||
testQUICConfig,
|
||||
udpListenerAddr,
|
||||
nil,
|
||||
index,
|
||||
tlsClientConfig,
|
||||
&mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}},
|
||||
&tunnelpogs.ConnectionOptions{},
|
||||
fakeControlStream{},
|
||||
&log,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return qc
|
||||
}
|
||||
|
||||
type mockReaderNoopWriter struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (m *mockReaderNoopWriter) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (m *mockReaderNoopWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
@@ -152,178 +151,3 @@ const (
|
||||
unregister rpcName = "unregister"
|
||||
authenticate rpcName = " authenticate"
|
||||
)
|
||||
|
||||
func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter CredentialManager, classicTunnel *ClassicTunnelProperties, registrationOptions *tunnelpogs.RegistrationOptions) error {
|
||||
h.observer.sendRegisteringEvent(registrationOptions.ConnectionID)
|
||||
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpcClient := NewTunnelServerClient(ctx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
_ = h.logServerInfo(ctx, rpcClient)
|
||||
registration := rpcClient.client.RegisterTunnel(
|
||||
ctx,
|
||||
classicTunnel.OriginCert,
|
||||
classicTunnel.Hostname,
|
||||
registrationOptions,
|
||||
)
|
||||
if registrationErr := registration.DeserializeError(); registrationErr != nil {
|
||||
// RegisterTunnel RPC failure
|
||||
return h.processRegisterTunnelError(registrationErr, register)
|
||||
}
|
||||
|
||||
credentialSetter.SetEventDigest(h.connIndex, registration.EventDigest)
|
||||
return h.processRegistrationSuccess(registration, register, credentialSetter, classicTunnel)
|
||||
}
|
||||
|
||||
type CredentialManager interface {
|
||||
ReconnectToken() ([]byte, error)
|
||||
EventDigest(connID uint8) ([]byte, error)
|
||||
SetEventDigest(connID uint8, digest []byte)
|
||||
ConnDigest(connID uint8) ([]byte, error)
|
||||
SetConnDigest(connID uint8, digest []byte)
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) processRegistrationSuccess(
|
||||
registration *tunnelpogs.TunnelRegistration,
|
||||
name rpcName,
|
||||
credentialManager CredentialManager, classicTunnel *ClassicTunnelProperties,
|
||||
) error {
|
||||
for _, logLine := range registration.LogLines {
|
||||
h.observer.log.Info().Msg(logLine)
|
||||
}
|
||||
|
||||
if registration.TunnelID != "" {
|
||||
h.observer.metrics.tunnelsHA.AddTunnelID(h.connIndex, registration.TunnelID)
|
||||
h.observer.log.Info().Msgf("Each HA connection's tunnel IDs: %v", h.observer.metrics.tunnelsHA.String())
|
||||
}
|
||||
|
||||
credentialManager.SetConnDigest(h.connIndex, registration.ConnDigest)
|
||||
h.observer.metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
|
||||
|
||||
h.observer.log.Info().Msgf("Route propagating, it may take up to 1 minute for your new route to become functional")
|
||||
h.observer.metrics.regSuccess.WithLabelValues(string(name)).Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, name rpcName) error {
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
h.observer.metrics.regFail.WithLabelValues("dup_edge_conn", string(name)).Inc()
|
||||
return errDuplicationConnection
|
||||
}
|
||||
h.observer.metrics.regFail.WithLabelValues("server_error", string(name)).Inc()
|
||||
return ServerRegisterTunnelError{
|
||||
Cause: err,
|
||||
Permanent: err.IsPermanent(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) reconnectTunnel(ctx context.Context, credentialManager CredentialManager, classicTunnel *ClassicTunnelProperties, registrationOptions *tunnelpogs.RegistrationOptions) error {
|
||||
token, err := credentialManager.ReconnectToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eventDigest, err := credentialManager.EventDigest(h.connIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connDigest, err := credentialManager.ConnDigest(h.connIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.observer.log.Debug().Msg("initiating RPC stream to reconnect")
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpcClient := NewTunnelServerClient(ctx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
_ = h.logServerInfo(ctx, rpcClient)
|
||||
registration := rpcClient.client.ReconnectTunnel(
|
||||
ctx,
|
||||
token,
|
||||
eventDigest,
|
||||
connDigest,
|
||||
classicTunnel.Hostname,
|
||||
registrationOptions,
|
||||
)
|
||||
if registrationErr := registration.DeserializeError(); registrationErr != nil {
|
||||
// ReconnectTunnel RPC failure
|
||||
return h.processRegisterTunnelError(registrationErr, reconnect)
|
||||
}
|
||||
return h.processRegistrationSuccess(registration, reconnect, credentialManager, classicTunnel)
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) logServerInfo(ctx context.Context, rpcClient *tunnelServerClient) error {
|
||||
// Request server info without blocking tunnel registration; must use capnp library directly.
|
||||
serverInfoPromise := tunnelrpc.TunnelServer{Client: rpcClient.client.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
|
||||
return nil
|
||||
})
|
||||
serverInfoMessage, err := serverInfoPromise.Result().Struct()
|
||||
if err != nil {
|
||||
h.observer.log.Err(err).Msg("Failed to retrieve server information")
|
||||
return err
|
||||
}
|
||||
serverInfo, err := tunnelpogs.UnmarshalServerInfo(serverInfoMessage)
|
||||
if err != nil {
|
||||
h.observer.log.Err(err).Msg("Failed to retrieve server information")
|
||||
return err
|
||||
}
|
||||
h.observer.logServerInfo(h.connIndex, serverInfo.LocationName, net.IP{}, "Connection established")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) registerNamedTunnel(
|
||||
ctx context.Context,
|
||||
namedTunnel *NamedTunnelProperties,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
) error {
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rpcClient := h.newRPCClientFunc(ctx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
registrationDetails, err := rpcClient.RegisterConnection(ctx, namedTunnel, connOptions, h.connIndex, nil, h.observer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.observer.logServerInfo(h.connIndex, registrationDetails.Location, nil, fmt.Sprintf("Connection %s registered", registrationDetails.UUID))
|
||||
h.observer.sendConnectedEvent(h.connIndex, H2mux, registrationDetails.Location)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) unregister(isNamedTunnel bool) {
|
||||
h.observer.sendUnregisteringEvent(h.connIndex)
|
||||
|
||||
unregisterCtx, cancel := context.WithTimeout(context.Background(), h.gracePeriod)
|
||||
defer cancel()
|
||||
|
||||
stream, err := h.newRPCStream(unregisterCtx, unregister)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
if isNamedTunnel {
|
||||
rpcClient := h.newRPCClientFunc(unregisterCtx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
rpcClient.GracefulShutdown(unregisterCtx, h.gracePeriod)
|
||||
} else {
|
||||
rpcClient := NewTunnelServerClient(unregisterCtx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
// gracePeriod is encoded in int64 using capnproto
|
||||
_ = rpcClient.client.UnregisterTunnel(unregisterCtx, h.gracePeriod.Nanoseconds())
|
||||
}
|
||||
|
||||
h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unregistered tunnel connection")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
const (
|
||||
logFieldOriginCertPath = "originCertPath"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
cert *OriginCert
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (c User) AccountID() string {
|
||||
return c.cert.AccountID
|
||||
}
|
||||
|
||||
func (c User) ZoneID() string {
|
||||
return c.cert.ZoneID
|
||||
}
|
||||
|
||||
func (c User) APIToken() string {
|
||||
return c.cert.APIToken
|
||||
}
|
||||
|
||||
func (c User) CertPath() string {
|
||||
return c.certPath
|
||||
}
|
||||
|
||||
// Client uses the user credentials to create a Cloudflare API client
|
||||
func (c *User) Client(apiURL string, userAgent string, log *zerolog.Logger) (cfapi.Client, error) {
|
||||
if apiURL == "" {
|
||||
return nil, errors.New("An api-url was not provided for the Cloudflare API client")
|
||||
}
|
||||
client, err := cfapi.NewRESTClient(
|
||||
apiURL,
|
||||
c.cert.AccountID,
|
||||
c.cert.ZoneID,
|
||||
c.cert.APIToken,
|
||||
userAgent,
|
||||
log,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Read will load and read the origin cert.pem to load the user credentials
|
||||
func Read(originCertPath string, log *zerolog.Logger) (*User, error) {
|
||||
originCertLog := log.With().
|
||||
Str(logFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCertPath, err := FindOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
blocks, err := readOriginCert(originCertPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := decodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
}
|
||||
|
||||
if cert.AccountID == "" {
|
||||
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
|
||||
}
|
||||
|
||||
return &User{
|
||||
cert: cert,
|
||||
certPath: originCertPath,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCredentialsRead(t *testing.T) {
|
||||
file, err := os.ReadFile("test-cloudflare-tunnel-cert-json.pem")
|
||||
require.NoError(t, err)
|
||||
dir := t.TempDir()
|
||||
certPath := path.Join(dir, originCertFile)
|
||||
os.WriteFile(certPath, file, fs.ModePerm)
|
||||
user, err := Read(certPath, &nopLog)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, certPath, user.CertPath())
|
||||
require.Equal(t, "test-service-key", user.APIToken())
|
||||
require.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", user.ZoneID())
|
||||
require.Equal(t, "abcdabcdabcdabcd1234567890abcdef", user.AccountID())
|
||||
}
|
||||
|
||||
func TestCredentialsClient(t *testing.T) {
|
||||
user := User{
|
||||
certPath: "/tmp/cert.pem",
|
||||
cert: &OriginCert{
|
||||
ZoneID: "7b0a4d77dfb881c1a3b7d61ea9443e19",
|
||||
AccountID: "abcdabcdabcdabcd1234567890abcdef",
|
||||
APIToken: "test-service-key",
|
||||
},
|
||||
}
|
||||
client, err := user.Client("example.com", "cloudflared/test", &nopLog)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCredentialFile = "cert.pem"
|
||||
OriginCertFlag = "origincert"
|
||||
)
|
||||
|
||||
type namedTunnelToken struct {
|
||||
ZoneID string `json:"zoneID"`
|
||||
AccountID string `json:"accountID"`
|
||||
APIToken string `json:"apiToken"`
|
||||
}
|
||||
|
||||
type OriginCert struct {
|
||||
ZoneID string
|
||||
APIToken string
|
||||
AccountID string
|
||||
}
|
||||
|
||||
// FindDefaultOriginCertPath returns the first path that contains a cert.pem file. If none of the
|
||||
// DefaultConfigSearchDirectories contains a cert.pem file, return empty string
|
||||
func FindDefaultOriginCertPath() string {
|
||||
for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() {
|
||||
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, DefaultCredentialFile))
|
||||
if ok := fileExists(originCertPath); ok {
|
||||
return originCertPath
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func decodeOriginCert(blocks []byte) (*OriginCert, error) {
|
||||
if len(blocks) == 0 {
|
||||
return nil, fmt.Errorf("Cannot decode empty certificate")
|
||||
}
|
||||
originCert := OriginCert{}
|
||||
block, rest := pem.Decode(blocks)
|
||||
for {
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
switch block.Type {
|
||||
case "PRIVATE KEY", "CERTIFICATE":
|
||||
// this is for legacy purposes.
|
||||
break
|
||||
case "ARGO TUNNEL TOKEN":
|
||||
if originCert.ZoneID != "" || originCert.APIToken != "" {
|
||||
return nil, fmt.Errorf("Found multiple tokens in the certificate")
|
||||
}
|
||||
// The token is a string,
|
||||
// Try the newer JSON format
|
||||
ntt := namedTunnelToken{}
|
||||
if err := json.Unmarshal(block.Bytes, &ntt); err == nil {
|
||||
originCert.ZoneID = ntt.ZoneID
|
||||
originCert.APIToken = ntt.APIToken
|
||||
originCert.AccountID = ntt.AccountID
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown block %s in the certificate", block.Type)
|
||||
}
|
||||
block, rest = pem.Decode(rest)
|
||||
}
|
||||
|
||||
if originCert.ZoneID == "" || originCert.APIToken == "" {
|
||||
return nil, fmt.Errorf("Missing token in the certificate")
|
||||
}
|
||||
|
||||
return &originCert, nil
|
||||
}
|
||||
|
||||
func readOriginCert(originCertPath string) ([]byte, error) {
|
||||
originCert, err := os.ReadFile(originCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read %s to load origin certificate", originCertPath)
|
||||
}
|
||||
|
||||
return originCert, nil
|
||||
}
|
||||
|
||||
// FindOriginCert will check to make sure that the certificate exists at the specified file path.
|
||||
func FindOriginCert(originCertPath string, log *zerolog.Logger) (string, error) {
|
||||
if originCertPath == "" {
|
||||
log.Error().Msgf("Cannot determine default origin certificate path. No file %s in %v. You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable", DefaultCredentialFile, config.DefaultConfigSearchDirectories())
|
||||
return "", fmt.Errorf("client didn't specify origincert path")
|
||||
}
|
||||
var err error
|
||||
originCertPath, err = homedir.Expand(originCertPath)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Cannot resolve origin certificate path")
|
||||
return "", fmt.Errorf("cannot resolve path %s", originCertPath)
|
||||
}
|
||||
// Check that the user has acquired a certificate using the login command
|
||||
ok := fileExists(originCertPath)
|
||||
if !ok {
|
||||
log.Error().Msgf(`Cannot find a valid certificate for your origin at the path:
|
||||
|
||||
%s
|
||||
|
||||
If the path above is wrong, specify the path with the -origincert option.
|
||||
If you don't have a certificate signed by Cloudflare, run the command:
|
||||
|
||||
cloudflared login
|
||||
`, originCertPath)
|
||||
return "", fmt.Errorf("cannot find a valid certificate at the path %s", originCertPath)
|
||||
}
|
||||
|
||||
return originCertPath, nil
|
||||
}
|
||||
|
||||
// FileExists checks to see if a file exist at the provided path.
|
||||
func fileExists(path string) bool {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fileStat.IsDir()
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
originCertFile = "cert.pem"
|
||||
)
|
||||
|
||||
var (
|
||||
nopLog = zerolog.Nop().With().Logger()
|
||||
)
|
||||
|
||||
func TestLoadOriginCert(t *testing.T) {
|
||||
cert, err := decodeOriginCert([]byte{})
|
||||
assert.Equal(t, fmt.Errorf("Cannot decode empty certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
|
||||
blocks, err := os.ReadFile("test-cert-unknown-block.pem")
|
||||
assert.NoError(t, err)
|
||||
cert, err = decodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Unknown block RSA PRIVATE KEY in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
}
|
||||
|
||||
func TestJSONArgoTunnelTokenEmpty(t *testing.T) {
|
||||
blocks, err := os.ReadFile("test-cert-no-token.pem")
|
||||
assert.NoError(t, err)
|
||||
cert, err := decodeOriginCert(blocks)
|
||||
assert.Equal(t, fmt.Errorf("Missing token in the certificate"), err)
|
||||
assert.Nil(t, cert)
|
||||
}
|
||||
|
||||
func TestJSONArgoTunnelToken(t *testing.T) {
|
||||
// The given cert's Argo Tunnel Token was generated by base64 encoding this JSON:
|
||||
// {
|
||||
// "zoneID": "7b0a4d77dfb881c1a3b7d61ea9443e19",
|
||||
// "apiToken": "test-service-key",
|
||||
// "accountID": "abcdabcdabcdabcd1234567890abcdef"
|
||||
// }
|
||||
CloudflareTunnelTokenTest(t, "test-cloudflare-tunnel-cert-json.pem")
|
||||
}
|
||||
|
||||
func CloudflareTunnelTokenTest(t *testing.T, path string) {
|
||||
blocks, err := os.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
cert, err := decodeOriginCert(blocks)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, cert)
|
||||
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
|
||||
key := "test-service-key"
|
||||
assert.Equal(t, key, cert.APIToken)
|
||||
}
|
||||
|
||||
type mockFile struct {
|
||||
path string
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
|
||||
type mockFileSystem struct {
|
||||
files map[string]mockFile
|
||||
}
|
||||
|
||||
func newMockFileSystem(files ...mockFile) *mockFileSystem {
|
||||
fs := mockFileSystem{map[string]mockFile{}}
|
||||
for _, f := range files {
|
||||
fs.files[f.path] = f
|
||||
}
|
||||
return &fs
|
||||
}
|
||||
|
||||
func (fs *mockFileSystem) ReadFile(path string) ([]byte, error) {
|
||||
if f, ok := fs.files[path]; ok {
|
||||
return f.data, f.err
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (fs *mockFileSystem) ValidFilePath(path string) bool {
|
||||
_, exists := fs.files[path]
|
||||
return exists
|
||||
}
|
||||
|
||||
func TestFindOriginCert_Valid(t *testing.T) {
|
||||
file, err := os.ReadFile("test-cloudflare-tunnel-cert-json.pem")
|
||||
require.NoError(t, err)
|
||||
dir := t.TempDir()
|
||||
certPath := path.Join(dir, originCertFile)
|
||||
os.WriteFile(certPath, file, fs.ModePerm)
|
||||
path, err := FindOriginCert(certPath, &nopLog)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, certPath, path)
|
||||
}
|
||||
|
||||
func TestFindOriginCert_Missing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath := path.Join(dir, originCertFile)
|
||||
_, err := FindOriginCert(certPath, &nopLog)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -51,7 +51,6 @@ K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN ARGO TUNNEL TOKEN-----
|
||||
eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAi
|
||||
c2VydmljZUtleSI6ICJ0ZXN0LXNlcnZpY2Uta2V5IiwgImFjY291bnRJRCI6ICJh
|
||||
YmNkYWJjZGFiY2RhYmNkMTIzNDU2Nzg5MGFiY2RlZiJ9
|
||||
eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAiYWNjb3VudElE
|
||||
IjogImFiY2RhYmNkYWJjZGFiY2QxMjM0NTY3ODkwYWJjZGVmIn0=
|
||||
-----END ARGO TUNNEL TOKEN-----
|
||||
@@ -50,7 +50,7 @@ cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
|
||||
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN WARP TOKEN-----
|
||||
-----BEGIN ARGO TUNNEL TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
|
||||
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
|
||||
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
|
||||
@@ -58,7 +58,7 @@ NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
|
||||
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
|
||||
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
|
||||
ZWYxZTI2Zjc=
|
||||
-----END WARP TOKEN-----
|
||||
-----END ARGO TUNNEL TOKEN-----
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
|
||||
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
|
||||
+3
-2
@@ -51,6 +51,7 @@ K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
|
||||
x+Yo/cL8fGfVpPt4UM8=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN ARGO TUNNEL TOKEN-----
|
||||
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdGVzdC1zZXJ2aWNlLWtl
|
||||
eQ==
|
||||
eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAiYXBpVG9rZW4i
|
||||
OiAidGVzdC1zZXJ2aWNlLWtleSIsICJhY2NvdW50SUQiOiAiYWJjZGFiY2RhYmNkYWJjZDEyMzQ1
|
||||
Njc4OTBhYmNkZWYifQ==
|
||||
-----END ARGO TUNNEL TOKEN-----
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,7 +38,7 @@ type manager struct {
|
||||
registrationChan chan *registerSessionEvent
|
||||
unregistrationChan chan *unregisterSessionEvent
|
||||
sendFunc transportSender
|
||||
receiveChan <-chan *quicpogs.SessionDatagram
|
||||
receiveChan <-chan *packet.Session
|
||||
closedChan <-chan struct{}
|
||||
sessions map[uuid.UUID]*Session
|
||||
log *zerolog.Logger
|
||||
@@ -45,7 +46,7 @@ type manager struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewManager(log *zerolog.Logger, sendF transportSender, receiveChan <-chan *quicpogs.SessionDatagram) *manager {
|
||||
func NewManager(log *zerolog.Logger, sendF transportSender, receiveChan <-chan *packet.Session) *manager {
|
||||
return &manager{
|
||||
registrationChan: make(chan *registerSessionEvent),
|
||||
unregistrationChan: make(chan *unregisterSessionEvent),
|
||||
@@ -120,7 +121,9 @@ func (m *manager) registerSession(ctx context.Context, registration *registerSes
|
||||
}
|
||||
|
||||
func (m *manager) newSession(id uuid.UUID, dstConn io.ReadWriteCloser) *Session {
|
||||
logger := m.log.With().Str("sessionID", id.String()).Logger()
|
||||
logger := m.log.With().
|
||||
Int(management.EventTypeKey, int(management.UDP)).
|
||||
Str("sessionID", id.String()).Logger()
|
||||
return &Session{
|
||||
ID: id,
|
||||
sendFunc: m.sendFunc,
|
||||
@@ -163,7 +166,7 @@ func (m *manager) unregisterSession(unregistration *unregisterSessionEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) sendToSession(datagram *quicpogs.SessionDatagram) {
|
||||
func (m *manager) sendToSession(datagram *packet.Session) {
|
||||
session, ok := m.sessions[datagram.ID]
|
||||
if !ok {
|
||||
m.log.Error().Str("sessionID", datagram.ID.String()).Msg("session not found")
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,7 +29,7 @@ func TestManagerServe(t *testing.T) {
|
||||
remoteUnregisterMsg = "eyeball closed connection"
|
||||
)
|
||||
|
||||
requestChan := make(chan *quicpogs.SessionDatagram)
|
||||
requestChan := make(chan *packet.Session)
|
||||
transport := mockQUICTransport{
|
||||
sessions: make(map[uuid.UUID]chan []byte),
|
||||
}
|
||||
@@ -241,9 +241,9 @@ type mockQUICTransport struct {
|
||||
sessions map[uuid.UUID]chan []byte
|
||||
}
|
||||
|
||||
func (me *mockQUICTransport) MuxSession(id uuid.UUID, payload []byte) error {
|
||||
session := me.sessions[id]
|
||||
session <- payload
|
||||
func (me *mockQUICTransport) MuxSession(session *packet.Session) error {
|
||||
s := me.sessions[session.ID]
|
||||
s <- session.Payload
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -255,9 +255,9 @@ type mockEyeballSession struct {
|
||||
respReceiver <-chan []byte
|
||||
}
|
||||
|
||||
func (me *mockEyeballSession) serve(ctx context.Context, requestChan chan *quicpogs.SessionDatagram) error {
|
||||
func (me *mockEyeballSession) serve(ctx context.Context, requestChan chan *packet.Session) error {
|
||||
for i := 0; i < me.expectedMsgCount; i++ {
|
||||
requestChan <- &quicpogs.SessionDatagram{
|
||||
requestChan <- &packet.Session{
|
||||
ID: me.id,
|
||||
Payload: me.expectedMsg,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package datagramsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,7 +22,14 @@ func SessionIdleErr(timeout time.Duration) error {
|
||||
return fmt.Errorf("session idle for %v", timeout)
|
||||
}
|
||||
|
||||
type transportSender func(sessionID uuid.UUID, payload []byte) error
|
||||
type transportSender func(session *packet.Session) error
|
||||
|
||||
// ErrVithVariableSeverity are errors that have variable severity
|
||||
type ErrVithVariableSeverity interface {
|
||||
error
|
||||
// LogLevel return the severity of this error
|
||||
LogLevel() zerolog.Level
|
||||
}
|
||||
|
||||
// Session is a bidirectional pipe of datagrams between transport and dstConn
|
||||
// Destination can be a connection with origin or with eyeball
|
||||
@@ -47,10 +57,14 @@ func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) (clos
|
||||
readBuffer := make([]byte, maxPacketSize)
|
||||
for {
|
||||
if closeSession, err := s.dstToTransport(readBuffer); err != nil {
|
||||
if err != net.ErrClosed {
|
||||
s.log.Error().Err(err).Msg("Failed to send session payload from destination to transport")
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
s.log.Debug().Msg("Destination connection closed")
|
||||
} else {
|
||||
s.log.Debug().Msg("Session cannot read from destination because the connection is closed")
|
||||
level := zerolog.ErrorLevel
|
||||
if variableErr, ok := err.(ErrVithVariableSeverity); ok {
|
||||
level = variableErr.LogLevel()
|
||||
}
|
||||
s.log.WithLevel(level).Err(err).Msg("Failed to send session payload from destination to transport")
|
||||
}
|
||||
if closeSession {
|
||||
s.closeChan <- err
|
||||
@@ -101,7 +115,11 @@ func (s *Session) dstToTransport(buffer []byte) (closeSession bool, err error) {
|
||||
s.markActive()
|
||||
// https://pkg.go.dev/io#Reader suggests caller should always process n > 0 bytes
|
||||
if n > 0 || err == nil {
|
||||
if sendErr := s.sendFunc(s.ID, buffer[:n]); sendErr != nil {
|
||||
session := packet.Session{
|
||||
ID: s.ID,
|
||||
Payload: buffer[:n],
|
||||
}
|
||||
if sendErr := s.sendFunc(&session); sendErr != nil {
|
||||
return false, sendErr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
)
|
||||
|
||||
// TestCloseSession makes sure a session will stop after context is done
|
||||
@@ -118,7 +118,7 @@ func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool)
|
||||
cfdConn, originConn := net.Pipe()
|
||||
payload := testPayload(sessionID)
|
||||
|
||||
respChan := make(chan *quicpogs.SessionDatagram)
|
||||
respChan := make(chan *packet.Session)
|
||||
sender := newMockTransportSender(sessionID, payload)
|
||||
mg := NewManager(&nopLogger, sender.muxSession, respChan)
|
||||
session := mg.newSession(sessionID, cfdConn)
|
||||
@@ -243,12 +243,12 @@ func newMockTransportSender(expectedSessionID uuid.UUID, expectedPayload []byte)
|
||||
}
|
||||
}
|
||||
|
||||
func (mts *mockTransportSender) muxSession(sessionID uuid.UUID, payload []byte) error {
|
||||
if sessionID != mts.expectedSessionID {
|
||||
return fmt.Errorf("Expect session %s, got %s", mts.expectedSessionID, sessionID)
|
||||
func (mts *mockTransportSender) muxSession(session *packet.Session) error {
|
||||
if session.ID != mts.expectedSessionID {
|
||||
return fmt.Errorf("Expect session %s, got %s", mts.expectedSessionID, session.ID)
|
||||
}
|
||||
if !bytes.Equal(payload, mts.expectedPayload) {
|
||||
return fmt.Errorf("Expect %v, read %v", mts.expectedPayload, payload)
|
||||
if !bytes.Equal(session.Payload, mts.expectedPayload) {
|
||||
return fmt.Errorf("Expect %v, read %v", mts.expectedPayload, session.Payload)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -258,7 +258,7 @@ type sendOnceTransportSender struct {
|
||||
sentChan chan struct{}
|
||||
}
|
||||
|
||||
func (sots *sendOnceTransportSender) muxSession(sessionID uuid.UUID, payload []byte) error {
|
||||
func (sots *sendOnceTransportSender) muxSession(session *packet.Session) error {
|
||||
defer close(sots.sentChan)
|
||||
return sots.baseSender.muxSession(sessionID, payload)
|
||||
return sots.baseSender.muxSession(session)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.18.1 as builder
|
||||
FROM golang:1.19 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,6 +43,19 @@ const (
|
||||
IPv6Only ConfigIPVersion = 6
|
||||
)
|
||||
|
||||
func (c ConfigIPVersion) String() string {
|
||||
switch c {
|
||||
case Auto:
|
||||
return "auto"
|
||||
case IPv4Only:
|
||||
return "4"
|
||||
case IPv6Only:
|
||||
return "6"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// IPVersion is the IP version of an EdgeAddr
|
||||
type EdgeIPVersion int8
|
||||
|
||||
@@ -71,11 +86,14 @@ type EdgeAddr struct {
|
||||
// If the call to net.LookupSRV fails, try to fall back to DoT from Cloudflare directly.
|
||||
//
|
||||
// Note: Instead of DoT, we could also have used DoH. Either of these:
|
||||
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv)
|
||||
// - indirectly via `tunneldns.NewUpstreamHTTPS()`
|
||||
// - directly via the JSON API (https://1.1.1.1/dns-query?ct=application/dns-json&name=_origintunneld._tcp.argotunnel.com&type=srv)
|
||||
// - indirectly via `tunneldns.NewUpstreamHTTPS()`
|
||||
//
|
||||
// But both of these cases miss out on a key feature from the stdlib:
|
||||
// "The returned records are sorted by priority and randomized by weight within a priority."
|
||||
// (https://golang.org/pkg/net/#Resolver.LookupSRV)
|
||||
//
|
||||
// "The returned records are sorted by priority and randomized by weight within a priority."
|
||||
// (https://golang.org/pkg/net/#Resolver.LookupSRV)
|
||||
//
|
||||
// Does this matter? I don't know. It may someday. Let's use DoT so we don't need to worry about it.
|
||||
// See also: Go feature request for stdlib-supported DoH: https://github.com/golang/go/issues/27552
|
||||
var fallbackLookupSRV = lookupSRVWithDOT
|
||||
@@ -92,16 +110,20 @@ var friendlyDNSErrorLines = []string{
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
|
||||
log.Debug().Str("domain", "_"+srvService+"._"+srvProto+"."+srvName).Msg("looking up edge SRV record")
|
||||
logger := log.With().Int(management.EventTypeKey, int(management.Cloudflared)).Logger()
|
||||
logger.Debug().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Str("domain", "_"+srvService+"._"+srvProto+"."+srvName).
|
||||
Msg("edge discovery: looking up edge SRV record")
|
||||
|
||||
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
|
||||
if fallbackErr != nil || len(fallbackAddrs) == 0 {
|
||||
// use the original DNS error `err` in messages, not `fallbackErr`
|
||||
log.Err(err).Msg("Error looking up Cloudflare edge IPs: the DNS query failed")
|
||||
logger.Err(err).Msg("edge discovery: error looking up Cloudflare edge IPs: the DNS query failed")
|
||||
for _, s := range friendlyDNSErrorLines {
|
||||
log.Error().Msg(s)
|
||||
logger.Error().Msg(s)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName)
|
||||
}
|
||||
@@ -115,9 +137,13 @@ func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range edgeAddrs {
|
||||
log.Debug().Msgf("Edge Address: %+v", *e)
|
||||
logAddrs := make([]string, len(edgeAddrs))
|
||||
for i, e := range edgeAddrs {
|
||||
logAddrs[i] = e.UDP.IP.String()
|
||||
}
|
||||
logger.Debug().
|
||||
Strs("addresses", logAddrs).
|
||||
Msg("edge discovery: resolved edge addresses")
|
||||
resolvedAddrPerCNAME = append(resolvedAddrPerCNAME, edgeAddrs)
|
||||
}
|
||||
|
||||
@@ -172,13 +198,15 @@ func ResolveAddrs(addrs []string, log *zerolog.Logger) (resolved []*EdgeAddr) {
|
||||
for _, addr := range addrs {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
log.Error().Str(logFieldAddress, addr).Err(err).Msg("failed to resolve to TCP address")
|
||||
log.Error().Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Str(logFieldAddress, addr).Err(err).Msg("edge discovery: failed to resolve to TCP address")
|
||||
continue
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
log.Error().Str(logFieldAddress, addr).Err(err).Msg("failed to resolve to UDP address")
|
||||
log.Error().Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Str(logFieldAddress, addr).Err(err).Msg("edge discovery: failed to resolve to UDP address")
|
||||
continue
|
||||
}
|
||||
version := V6
|
||||
|
||||
@@ -2,6 +2,7 @@ package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -85,6 +86,15 @@ func (rs *Regions) AddrUsedBy(connID int) *EdgeAddr {
|
||||
// GetUnusedAddr gets an unused addr from the edge, excluding the given addr. Prefer to use addresses
|
||||
// evenly across both regions.
|
||||
func (rs *Regions) GetUnusedAddr(excluding *EdgeAddr, connID int) *EdgeAddr {
|
||||
// If both regions have the same number of available addrs, lets randomise which one
|
||||
// we pick. The rest of this algorithm will continue to make sure we always use addresses
|
||||
// evenly across both regions.
|
||||
if rs.region1.AvailableAddrs() == rs.region2.AvailableAddrs() {
|
||||
regions := []Region{rs.region1, rs.region2}
|
||||
firstChoice := rand.Intn(2)
|
||||
return getAddrs(excluding, connID, ®ions[firstChoice], ®ions[1-firstChoice])
|
||||
}
|
||||
|
||||
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
|
||||
return getAddrs(excluding, connID, &rs.region1, &rs.region2)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,16 @@ func DialEdge(
|
||||
timeout time.Duration,
|
||||
tlsConfig *tls.Config,
|
||||
edgeTCPAddr *net.TCPAddr,
|
||||
localIP net.IP,
|
||||
) (net.Conn, error) {
|
||||
// Inherit from parent context so we can cancel (Ctrl-C) while dialing
|
||||
dialCtx, dialCancel := context.WithTimeout(ctx, timeout)
|
||||
defer dialCancel()
|
||||
|
||||
dialer := net.Dialer{}
|
||||
if localIP != nil {
|
||||
dialer.LocalAddr = &net.TCPAddr{IP: localIP, Port: 0}
|
||||
}
|
||||
edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeTCPAddr.String())
|
||||
if err != nil {
|
||||
return nil, newDialError(err, "DialContext error")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -74,33 +75,35 @@ func (ed *Edge) GetAddrForRPC() (*allregions.EdgeAddr, error) {
|
||||
|
||||
// GetAddr gives this proxy connection an edge Addr. Prefer Addrs this connection has already used.
|
||||
func (ed *Edge) GetAddr(connIndex int) (*allregions.EdgeAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
log := ed.log.With().
|
||||
Int(LogFieldConnIndex, connIndex).
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Logger()
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
|
||||
// If this connection has already used an edge addr, return it.
|
||||
if addr := ed.regions.AddrUsedBy(connIndex); addr != nil {
|
||||
log.Debug().Msg("edgediscovery - GetAddr: Returning same address back to proxy connection")
|
||||
log.Debug().IPAddr(LogFieldIPAddress, addr.UDP.IP).Msg("edge discovery: returning same edge address back to pool")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// Otherwise, give it an unused one
|
||||
addr := ed.regions.GetUnusedAddr(nil, connIndex)
|
||||
if addr == nil {
|
||||
log.Debug().Msg("edgediscovery - GetAddr: No addresses left to give proxy connection")
|
||||
log.Debug().Msg("edge discovery: no addresses left in pool to give proxy connection")
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
log = ed.log.With().
|
||||
Int(LogFieldConnIndex, connIndex).
|
||||
IPAddr(LogFieldIPAddress, addr.UDP.IP).Logger()
|
||||
log.Debug().Msgf("edgediscovery - GetAddr: Giving connection its new address")
|
||||
log.Debug().IPAddr(LogFieldIPAddress, addr.UDP.IP).Msg("edge discovery: giving new address to connection")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// GetDifferentAddr gives back the proxy connection's edge Addr and uses a new one.
|
||||
func (ed *Edge) GetDifferentAddr(connIndex int, hasConnectivityError bool) (*allregions.EdgeAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
|
||||
log := ed.log.With().
|
||||
Int(LogFieldConnIndex, connIndex).
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
Logger()
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
|
||||
@@ -110,14 +113,14 @@ func (ed *Edge) GetDifferentAddr(connIndex int, hasConnectivityError bool) (*all
|
||||
}
|
||||
addr := ed.regions.GetUnusedAddr(oldAddr, connIndex)
|
||||
if addr == nil {
|
||||
log.Debug().Msg("edgediscovery - GetDifferentAddr: No addresses left to give proxy connection")
|
||||
log.Debug().Msg("edge discovery: no addresses left in pool to give proxy connection")
|
||||
// note: if oldAddr were not nil, it will become available on the next iteration
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
log = ed.log.With().
|
||||
Int(LogFieldConnIndex, connIndex).
|
||||
IPAddr(LogFieldIPAddress, addr.UDP.IP).Logger()
|
||||
log.Debug().Msgf("edgediscovery - GetDifferentAddr: Giving connection its new address from the address list: %v", ed.regions.AvailableAddrs())
|
||||
log.Debug().
|
||||
IPAddr(LogFieldIPAddress, addr.UDP.IP).
|
||||
Int("available", ed.regions.AvailableAddrs()).
|
||||
Msg("edge discovery: giving new address to connection")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
@@ -133,8 +136,9 @@ func (ed *Edge) AvailableAddrs() int {
|
||||
func (ed *Edge) GiveBack(addr *allregions.EdgeAddr, hasConnectivityError bool) bool {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
log := ed.log.With().
|
||||
IPAddr(LogFieldIPAddress, addr.UDP.IP).Logger()
|
||||
log.Debug().Msgf("edgediscovery - GiveBack: Address now unused")
|
||||
ed.log.Debug().
|
||||
Int(management.EventTypeKey, int(management.Cloudflared)).
|
||||
IPAddr(LogFieldIPAddress, addr.UDP.IP).
|
||||
Msg("edge discovery: gave back address to the pool")
|
||||
return ed.regions.GiveBack(addr, hasConnectivityError)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ var (
|
||||
errNoProtocolRecord = fmt.Errorf("No TXT record found for %s to determine connection protocol", protocolRecord)
|
||||
)
|
||||
|
||||
type PercentageFetcher func() (ProtocolPercents, error)
|
||||
|
||||
// ProtocolPercent represents a single Protocol Percentage combination.
|
||||
type ProtocolPercent struct {
|
||||
Protocol string `json:"protocol"`
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package features
|
||||
|
||||
const (
|
||||
FeatureSerializedHeaders = "serialized_headers"
|
||||
FeatureQuickReconnects = "quick_reconnects"
|
||||
FeatureAllowRemoteConfig = "allow_remote_config"
|
||||
FeatureDatagramV2 = "support_datagram_v2"
|
||||
FeaturePostQuantum = "postquantum"
|
||||
FeatureQUICSupportEOF = "support_quic_eof"
|
||||
FeatureManagementLogs = "management_logs"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultFeatures = []string{
|
||||
FeatureAllowRemoteConfig,
|
||||
FeatureSerializedHeaders,
|
||||
FeatureDatagramV2,
|
||||
FeatureQUICSupportEOF,
|
||||
FeatureManagementLogs,
|
||||
}
|
||||
)
|
||||
|
||||
func Contains(feature string) bool {
|
||||
for _, f := range DefaultFeatures {
|
||||
if f == feature {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,4 +2,11 @@
|
||||
|
||||
package main
|
||||
|
||||
import _ "crypto/tls/fipsonly"
|
||||
import (
|
||||
_ "crypto/tls/fipsonly"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
)
|
||||
|
||||
func init () {
|
||||
tunnel.FipsEnabled = true
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,12 +2,12 @@
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
OUTPUT=$(for d in $(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -d $d ; done)
|
||||
OUTPUT=$(goimports -l -d -local github.com/cloudflare/cloudflared $(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc))
|
||||
|
||||
if [ -n "$OUTPUT" ] ; then
|
||||
PAGER=$(which colordiff || echo cat)
|
||||
echo
|
||||
echo "Code formatting issues found, use 'goimports -format-only -local github.com/cloudflare/cloudflared' to correct them"
|
||||
echo "Code formatting issues found, use 'make fmt' to correct them"
|
||||
echo
|
||||
echo "$OUTPUT" | $PAGER
|
||||
exit 1
|
||||
|
||||
Regular → Executable
+74
-21
@@ -1,29 +1,56 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Creates Github Releases Notes with content hashes
|
||||
Create Github Releases Notes with binary checksums from Workers KV
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import hashlib
|
||||
import glob
|
||||
import requests
|
||||
|
||||
from github import Github, GithubException, UnknownObjectException
|
||||
from github import Github, UnknownObjectException
|
||||
|
||||
FORMAT = "%(levelname)s - %(asctime)s: %(message)s"
|
||||
logging.basicConfig(format=FORMAT)
|
||||
logging.basicConfig(format=FORMAT, level=logging.INFO)
|
||||
|
||||
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
|
||||
GITHUB_CONFLICT_CODE = "already_exists"
|
||||
BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/'
|
||||
|
||||
def get_sha256(filename):
|
||||
""" get the sha256 of a file """
|
||||
sha256_hash = hashlib.sha256()
|
||||
with open(filename,"rb") as f:
|
||||
for byte_block in iter(lambda: f.read(4096),b""):
|
||||
sha256_hash.update(byte_block)
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
def kv_get_keys(prefix, account, namespace, api_token):
|
||||
""" get the KV keys for a given prefix """
|
||||
response = requests.get(
|
||||
BASE_KV_URL + account + "/storage/kv/namespaces/" +
|
||||
namespace + "/keys" + "?prefix=" + prefix,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + api_token,
|
||||
},
|
||||
)
|
||||
if response.status_code != 200:
|
||||
jsonResponse = response.json()
|
||||
errors = jsonResponse["errors"]
|
||||
if len(errors) > 0:
|
||||
raise Exception("failed to get checksums: {0}", errors[0])
|
||||
return response.json()["result"]
|
||||
|
||||
|
||||
def kv_get_value(key, account, namespace, api_token):
|
||||
""" get the KV value for a provided key """
|
||||
response = requests.get(
|
||||
BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/values/" + key,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + api_token,
|
||||
},
|
||||
)
|
||||
if response.status_code != 200:
|
||||
jsonResponse = response.json()
|
||||
errors = jsonResponse["errors"]
|
||||
if len(errors) > 0:
|
||||
raise Exception("failed to get checksums: {0}", errors[0])
|
||||
return response.text
|
||||
|
||||
|
||||
def update_or_add_message(msg, name, sha):
|
||||
@@ -42,6 +69,7 @@ def update_or_add_message(msg, name, sha):
|
||||
return '{0}{1}```'.format(msg[:back], new_text)
|
||||
return '{0} \n### SHA256 Checksums:\n```\n{1}```'.format(msg, new_text)
|
||||
|
||||
|
||||
def get_release(repo, version):
|
||||
""" Get a Github Release matching the version tag. """
|
||||
try:
|
||||
@@ -55,11 +83,20 @@ def get_release(repo, version):
|
||||
def parse_args():
|
||||
""" Parse and validate args """
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Creates Github Releases and uploads assets."
|
||||
description="Updates a Github Release with checksums from KV"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-key", default=os.environ.get("API_KEY"), help="Github API key"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kv-namespace-id", default=os.environ.get("KV_NAMESPACE"), help="workers KV namespace id"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kv-account-id", default=os.environ.get("KV_ACCOUNT"), help="workers KV account id"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kv-api-token", default=os.environ.get("KV_API_TOKEN"), help="workers KV API Token"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--release-version",
|
||||
metavar="version",
|
||||
@@ -80,6 +117,18 @@ def parse_args():
|
||||
logging.error("Missing API key")
|
||||
is_valid = False
|
||||
|
||||
if not args.kv_namespace_id:
|
||||
logging.error("Missing KV namespace id")
|
||||
is_valid = False
|
||||
|
||||
if not args.kv_account_id:
|
||||
logging.error("Missing KV account id")
|
||||
is_valid = False
|
||||
|
||||
if not args.kv_api_token:
|
||||
logging.error("Missing KV API token")
|
||||
is_valid = False
|
||||
|
||||
if is_valid:
|
||||
return args
|
||||
|
||||
@@ -95,16 +144,20 @@ def main():
|
||||
repo = client.get_repo(CLOUDFLARED_REPO)
|
||||
release = get_release(repo, args.release_version)
|
||||
|
||||
msg = release.body
|
||||
msg = ""
|
||||
|
||||
for filepath in glob.glob("artifacts/*"):
|
||||
pkg_hash = get_sha256(filepath)
|
||||
# add the sha256 of the new artifact to the release message body
|
||||
name = os.path.basename(filepath)
|
||||
msg = update_or_add_message(msg, name, pkg_hash)
|
||||
prefix = f"update_{args.release_version}_"
|
||||
keys = kv_get_keys(prefix, args.kv_account_id,
|
||||
args.kv_namespace_id, args.kv_api_token)
|
||||
for key in [k["name"] for k in keys]:
|
||||
checksum = kv_get_value(
|
||||
key, args.kv_account_id, args.kv_namespace_id, args.kv_api_token)
|
||||
binary_name = key[len(prefix):]
|
||||
msg = update_or_add_message(msg, binary_name, checksum)
|
||||
|
||||
if args.dry_run:
|
||||
logging.info("Skipping asset upload because of dry-run")
|
||||
logging.info("Skipping release message update because of dry-run")
|
||||
logging.info(f"Github message:\n{msg}")
|
||||
return
|
||||
|
||||
# update the release body text
|
||||
@@ -115,4 +168,4 @@ def main():
|
||||
exit(1)
|
||||
|
||||
|
||||
main()
|
||||
main()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user