Compare commits
130 Commits
Author | SHA1 | Date |
---|---|---|
chungthuang | f27418044b | |
Devin Carr | 1b02d169ad | |
João "Pisco" Fernandes | 84833011ec | |
chungthuang | 5e5f2f4d8c | |
Devin Carr | b9898a9fbe | |
Devin Carr | 687682120c | |
Devin Carr | a1a9f3813e | |
GoncaloGarcia | 7deb4340b4 | |
Steven Kreitzer | b5be8a6fa4 | |
Alexandru Tocar | a665d3245a | |
chungthuang | a48691fe78 | |
chungthuang | b723a1a426 | |
GoncaloGarcia | bb29a0e194 | |
GoncaloGarcia | 86476e6248 | |
João "Pisco" Fernandes | da6fac4133 | |
João "Pisco" Fernandes | 47ad3238dd | |
João "Pisco" Fernandes | 4f7165530c | |
Nikita Sivukhin | a36fa07aba | |
Nanashi | e846943e66 | |
YueYue | 652c82daa9 | |
K.B.Dharun Krishna | a6760a6cbf | |
K.B.Dharun Krishna | 204d55ecec | |
K.B.Dharun Krishna | 1f4511ca6e | |
chungthuang | 110b2b4c80 | |
João Oliveirinha | dc2c76738a | |
João Oliveirinha | 5344a0bc6a | |
chungthuang | 3299a9bc15 | |
chungthuang | 34a876e4e7 | |
Devin Carr | 971360d5e0 | |
João "Pisco" Fernandes | 76badfa01b | |
Igor Postelnik | 56aeb6be65 | |
chungthuang | a9aa48d7a1 | |
chungthuang | 638203f9f1 | |
chungthuang | 98e043d17d | |
João Oliveirinha | 3ad4b732d4 | |
chungthuang | 9c1f5c33a8 | |
chungthuang | f75503bf3c | |
chungthuang | 2c38487a54 | |
chungthuang | ae0b261e56 | |
chungthuang | e653741885 | |
João Oliveirinha | e5ae80ab86 | |
chungthuang | ba2edca352 | |
Chung-Ting | c8ffdae859 | |
Chung-Ting | 8fc8c17522 | |
João "Pisco" Fernandes | 8d9aab5217 | |
João Oliveirinha | 25f91fec10 | |
chungthuang | c7b2cce131 | |
chungthuang | 3e5c2959db | |
chungthuang | 37ec2d4830 | |
chungthuang | ecd101d485 | |
chungthuang | cf5be91d2d | |
chungthuang | 28685a5055 | |
chungthuang | e23d928829 | |
chungthuang | 159fcb44ce | |
chungthuang | 8e69f41833 | |
Cristian Rodríguez | fbe357b1e6 | |
chungthuang | 00cd7c333c | |
chungthuang | 86b50eda15 | |
James Royal | 652df22831 | |
Shak Saleemi | 1776d3d335 | |
Chung-Ting | 33baad35b8 | |
Chung-Ting | 12dd91ada1 | |
Honahuku | b901d73d9b | |
Kyle Carberry | 61a16538a1 | |
TMKnight | 9e1f4c2bca | |
Alex Vanderpot | f51be82729 | |
Lars Lehtonen | fd5d8260bb | |
Sam Cook | f2c4fdb0ae | |
Lars Lehtonen | a4a84bb27e | |
Chung-Ting | 4ddc8d758b | |
Chung-Ting | 8068cdebb6 | |
James Royal | 45236a1f7d | |
Devin Carr | e0a55f9c0e | |
Sudarsan Reddy | c1d8c5e960 | |
Devin Carr | 7ae1d4668e | |
João Oliveirinha | adb7d40084 | |
João "Pisco" Fernandes | 541c63d737 | |
João Oliveirinha | f1d6f0c0be | |
João "Pisco" Fernandes | 958b6f1d24 | |
João Oliveirinha | 6d1d91d9f9 | |
João Oliveirinha | fc0ecf4185 | |
João Oliveirinha | 349586007c | |
Chung-Ting Huang | 569a7c3c9e | |
Chung-Ting Huang | bec683b67d | |
Chung-Ting Huang | 38d3c3cae5 | |
Chung-Ting Huang | f2d765351d | |
Sudarsan Reddy | 5d8f60873d | |
Chung-Ting Huang | b474778cf1 | |
Devin Carr | 65247b6f0f | |
Devin Carr | 5f3cfe044f | |
Devin Carr | 81fe0bd12b | |
João Oliveirinha | bfeaa3418d | |
Devin Carr | 9584adc38a | |
Devin Carr | 0096f2613c | |
João Oliveirinha | ac82c8b08b | |
João "Pisco" Fernandes | af3a66d60e | |
Devin Carr | 42e0540395 | |
Devin Carr | 2ee90483bf | |
Devin Carr | 2084a123c2 | |
Devin Carr | b500e556bf | |
Devin Carr | 1b0b6bf7a8 | |
Devin Carr | 85eee4849f | |
Devin Carr | 9b8a533435 | |
Devin Carr | 5abb90b539 | |
João Oliveirinha | 0c8bc56930 | |
Devin Carr | fdab68aa08 | |
Devin Carr | 5aaab967a3 | |
Devin Carr | ccad59dfab | |
Devin Carr | 8a3eade6d3 | |
Sudarsan Reddy | 39847a70f2 | |
João Oliveirinha | d1e338ee48 | |
Devin Carr | b243602d1c | |
Devin Carr | 960c5a7baf | |
Devin Carr | aca3575b6d | |
Devin Carr | 2b4815a9f5 | |
João "Pisco" Fernandes | 729890d847 | |
EduardoGomes | 31f424d589 | |
Sudarsan Reddy | cb4bd8d065 | |
Sudarsan Reddy | 1abd22ef0a | |
Devin Carr | a3bcf25fae | |
João Oliveirinha | 20e36c5bf3 | |
João "Pisco" Fernandes | 5693ba524b | |
João Oliveirinha | 9c6fbfca18 | |
João "Pisco" Fernandes | 925ec100d6 | |
Sudarsan Reddy | 58b27a1ccf | |
Devin Carr | 867360c8dd | |
Devin Carr | cb97257815 | |
Devin Carr | c43e07d6b7 | |
Devin Carr | 9426b60308 | |
Devin Carr | ff9621bbd5 |
|
@ -4,15 +4,15 @@ jobs:
|
||||||
check:
|
check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.19.x]
|
go-version: [1.21.x]
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Test
|
- name: Test
|
||||||
run: make test
|
run: make test
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# !/usr/bin/env bash
|
||||||
|
|
||||||
|
cd /tmp
|
||||||
|
git clone -q https://github.com/cloudflare/go
|
||||||
|
cd go/src
|
||||||
|
# https://github.com/cloudflare/go/tree/34129e47042e214121b6bbff0ded4712debed18e is version go1.21.5-devel-cf
|
||||||
|
git checkout -q 34129e47042e214121b6bbff0ded4712debed18e
|
||||||
|
./make.bash
|
|
@ -143,7 +143,7 @@ if [[ ! -z "$CODE_SIGN_NAME" ]]; then
|
||||||
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
|
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
|
||||||
|
|
||||||
# notarize the binary
|
# notarize the binary
|
||||||
# TODO: https://jira.cfdata.org/browse/TUN-5789
|
# TODO: TUN-5789
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# creating build directory
|
# creating build directory
|
||||||
|
@ -169,7 +169,7 @@ if [[ ! -z "$PKG_SIGN_NAME" ]]; then
|
||||||
${PKGNAME}
|
${PKGNAME}
|
||||||
|
|
||||||
# notarize the package
|
# notarize the package
|
||||||
# TODO: https://jira.cfdata.org/browse/TUN-5789
|
# TODO: TUN-5789
|
||||||
else
|
else
|
||||||
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
pkgbuild --identifier com.cloudflare.${PRODUCT} \
|
||||||
--version ${VERSION} \
|
--version ${VERSION} \
|
|
@ -0,0 +1,10 @@
|
||||||
|
rm -rf /tmp/go
|
||||||
|
export GOCACHE=/tmp/gocache
|
||||||
|
rm -rf $GOCACHE
|
||||||
|
|
||||||
|
./.teamcity/install-cloudflare-go.sh
|
||||||
|
|
||||||
|
export PATH="/tmp/go/bin:$PATH"
|
||||||
|
go version
|
||||||
|
which go
|
||||||
|
go env
|
|
@ -1,26 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
|
||||||
echo "Skipping public release for an untagged commit."
|
|
||||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${HOMEBREW_GITHUB_API_TOKEN:-}" == "" ]] ; then
|
|
||||||
echo "Missing GITHUB_API_TOKEN"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# "install" Homebrew
|
|
||||||
git clone https://github.com/Homebrew/brew tmp/homebrew
|
|
||||||
eval "$(tmp/homebrew/bin/brew shellenv)"
|
|
||||||
brew update --force --quiet
|
|
||||||
chmod -R go-w "$(brew --prefix)/share/zsh"
|
|
||||||
|
|
||||||
git config --global user.name "cloudflare-warp-bot"
|
|
||||||
git config --global user.email "warp-bot@cloudflare.com"
|
|
||||||
|
|
||||||
# bump formula pr
|
|
||||||
brew bump-formula-pr cloudflared --version="$VERSION" --no-browse --no-audit
|
|
|
@ -1,66 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
|
|
||||||
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
|
|
||||||
echo "Skipping public release for an untagged commit."
|
|
||||||
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$FILENAME" ]] ; then
|
|
||||||
echo "Missing $FILENAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${GITHUB_PRIVATE_KEY_B64:-}" == "" ]] ; then
|
|
||||||
echo "Missing GITHUB_PRIVATE_KEY_B64"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# upload to s3 bucket for use by Homebrew formula
|
|
||||||
s3cmd \
|
|
||||||
--acl-public --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
|
||||||
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
|
||||||
s3cmd \
|
|
||||||
--acl-public --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
|
|
||||||
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
|
|
||||||
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
|
|
||||||
|
|
||||||
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
|
|
||||||
mkdir -p tmp
|
|
||||||
ssh-keyscan -t rsa github.com > tmp/github.txt
|
|
||||||
echo "$GITHUB_PRIVATE_KEY_B64" | base64 --decode > tmp/private.key
|
|
||||||
chmod 0400 tmp/private.key
|
|
||||||
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
|
|
||||||
|
|
||||||
# clone Homebrew repo into tmp/homebrew-cloudflare
|
|
||||||
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
|
|
||||||
cd tmp/homebrew-cloudflare
|
|
||||||
git checkout -f master
|
|
||||||
git reset --hard origin/master
|
|
||||||
|
|
||||||
# modify cloudflared.rb
|
|
||||||
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
|
||||||
tee cloudflared.rb <<EOF
|
|
||||||
class Cloudflared < Formula
|
|
||||||
desc 'Cloudflare Tunnel'
|
|
||||||
homepage 'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps'
|
|
||||||
url '$URL'
|
|
||||||
sha256 '$SHA256'
|
|
||||||
version '$VERSION'
|
|
||||||
def install
|
|
||||||
bin.install 'cloudflared'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# push cloudflared.rb
|
|
||||||
git add cloudflared.rb
|
|
||||||
git diff
|
|
||||||
git config user.name "cloudflare-warp-bot"
|
|
||||||
git config user.email "warp-bot@cloudflare.com"
|
|
||||||
git commit -m "Release Cloudflare Tunnel $VERSION"
|
|
||||||
|
|
||||||
git push -v origin master
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
# Relative path to working directory
|
||||||
|
$CloudflaredDirectory = "go\src\github.com\cloudflare\cloudflared"
|
||||||
|
|
||||||
|
cd $CloudflaredDirectory
|
||||||
|
|
||||||
|
Write-Output "Building for amd64"
|
||||||
|
$env:TARGET_OS = "windows"
|
||||||
|
$env:CGO_ENABLED = 1
|
||||||
|
$env:TARGET_ARCH = "amd64"
|
||||||
|
$env:Path = "$Env:Temp\go\bin;$($env:Path)"
|
||||||
|
|
||||||
|
go env
|
||||||
|
go version
|
||||||
|
|
||||||
|
& make cloudflared
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for amd64" }
|
||||||
|
copy .\cloudflared.exe .\cloudflared-windows-amd64.exe
|
||||||
|
|
||||||
|
Write-Output "Building for 386"
|
||||||
|
$env:CGO_ENABLED = 0
|
||||||
|
$env:TARGET_ARCH = "386"
|
||||||
|
make cloudflared
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for 386" }
|
||||||
|
copy .\cloudflared.exe .\cloudflared-windows-386.exe
|
|
@ -0,0 +1,82 @@
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
$WorkingDirectory = Get-Location
|
||||||
|
$CloudflaredDirectory = "$WorkingDirectory\go\src\github.com\cloudflare\cloudflared"
|
||||||
|
|
||||||
|
Write-Output "Installing python..."
|
||||||
|
|
||||||
|
$PythonVersion = "3.10.11"
|
||||||
|
$PythonZipFile = "$env:Temp\python-$PythonVersion-embed-amd64.zip"
|
||||||
|
$PipInstallFile = "$env:Temp\get-pip.py"
|
||||||
|
$PythonZipUrl = "https://www.python.org/ftp/python/$PythonVersion/python-$PythonVersion-embed-amd64.zip"
|
||||||
|
$PythonPath = "$WorkingDirectory\Python"
|
||||||
|
$PythonBinPath = "$PythonPath\python.exe"
|
||||||
|
|
||||||
|
# Download Python zip file
|
||||||
|
Invoke-WebRequest -Uri $PythonZipUrl -OutFile $PythonZipFile
|
||||||
|
|
||||||
|
# Download Python pip file
|
||||||
|
Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile $PipInstallFile
|
||||||
|
|
||||||
|
# Extract Python files
|
||||||
|
Expand-Archive $PythonZipFile -DestinationPath $PythonPath -Force
|
||||||
|
|
||||||
|
# Add Python to PATH
|
||||||
|
$env:Path = "$PythonPath\Scripts;$PythonPath;$($env:Path)"
|
||||||
|
|
||||||
|
Write-Output "Installed to $PythonPath"
|
||||||
|
|
||||||
|
# Install pip
|
||||||
|
& $PythonBinPath $PipInstallFile
|
||||||
|
|
||||||
|
# Add package paths in pythonXX._pth to unblock python -m pip
|
||||||
|
$PythonImportPathFile = "$PythonPath\python310._pth"
|
||||||
|
$ComponentTestsDir = "$CloudflaredDirectory\component-tests\"
|
||||||
|
@($ComponentTestsDir, "Lib\site-packages", $(Get-Content $PythonImportPathFile)) | Set-Content $PythonImportPathFile
|
||||||
|
|
||||||
|
# Test Python installation
|
||||||
|
& $PythonBinPath --version
|
||||||
|
& $PythonBinPath -m pip --version
|
||||||
|
|
||||||
|
go env
|
||||||
|
go version
|
||||||
|
|
||||||
|
$env:TARGET_OS = "windows"
|
||||||
|
$env:CGO_ENABLED = 1
|
||||||
|
$env:TARGET_ARCH = "amd64"
|
||||||
|
$env:Path = "$Env:Temp\go\bin;$($env:Path)"
|
||||||
|
|
||||||
|
& $PythonBinPath --version
|
||||||
|
& $PythonBinPath -m pip --version
|
||||||
|
|
||||||
|
cd $CloudflaredDirectory
|
||||||
|
|
||||||
|
go env
|
||||||
|
go version
|
||||||
|
|
||||||
|
Write-Output "Building cloudflared"
|
||||||
|
|
||||||
|
& make cloudflared
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared" }
|
||||||
|
|
||||||
|
echo $LASTEXITCODE
|
||||||
|
|
||||||
|
Write-Output "Running unit tests"
|
||||||
|
|
||||||
|
# Not testing with race detector because of https://github.com/golang/go/issues/61058
|
||||||
|
# We already test it on other platforms
|
||||||
|
& go test -failfast -mod=vendor ./...
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Failed unit tests" }
|
||||||
|
|
||||||
|
Write-Output "Running component tests"
|
||||||
|
|
||||||
|
& $PythonBinPath -m pip install --upgrade -r component-tests/requirements.txt
|
||||||
|
& $PythonBinPath component-tests/setup.py --type create
|
||||||
|
& $PythonBinPath -m pytest component-tests -o log_cli=true --log-cli-level=INFO
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
& $PythonBinPath component-tests/setup.py --type cleanup
|
||||||
|
throw "Failed component tests"
|
||||||
|
}
|
||||||
|
& $PythonBinPath component-tests/setup.py --type cleanup
|
|
@ -0,0 +1,16 @@
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
Write-Output "Downloading cloudflare go..."
|
||||||
|
|
||||||
|
Set-Location "$Env:Temp"
|
||||||
|
|
||||||
|
git clone -q https://github.com/cloudflare/go
|
||||||
|
Write-Output "Building go..."
|
||||||
|
cd go/src
|
||||||
|
# https://github.com/cloudflare/go/tree/34129e47042e214121b6bbff0ded4712debed18e is version go1.21.5-devel-cf
|
||||||
|
git checkout -q 34129e47042e214121b6bbff0ded4712debed18e
|
||||||
|
& ./make.bat
|
||||||
|
|
||||||
|
Write-Output "Installed"
|
|
@ -0,0 +1,20 @@
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
$GoMsiVersion = "go1.21.5.windows-amd64.msi"
|
||||||
|
|
||||||
|
Write-Output "Downloading go installer..."
|
||||||
|
|
||||||
|
Set-Location "$Env:Temp"
|
||||||
|
|
||||||
|
(New-Object System.Net.WebClient).DownloadFile(
|
||||||
|
"https://go.dev/dl/$GoMsiVersion",
|
||||||
|
"$Env:Temp\$GoMsiVersion"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Output "Installing go..."
|
||||||
|
Install-Package "$Env:Temp\$GoMsiVersion" -Force
|
||||||
|
|
||||||
|
# Go installer updates global $PATH
|
||||||
|
go env
|
||||||
|
|
||||||
|
Write-Output "Installed"
|
11
CHANGES.md
11
CHANGES.md
|
@ -1,3 +1,14 @@
|
||||||
|
## 2024.2.1
|
||||||
|
### Notices
|
||||||
|
- Starting from this version, tunnel diagnostics will be enabled by default. This will allow the engineering team to remotely get diagnostics from cloudflared during debug activities. Users still have the capability to opt-out of this feature by defining `--management-diagnostics=false` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`).
|
||||||
|
|
||||||
|
## 2023.9.0
|
||||||
|
### Notices
|
||||||
|
- The `warp-routing` `enabled: boolean` flag is no longer supported in the configuration file. Warp Routing traffic (eg TCP, UDP, ICMP) traffic is proxied to cloudflared if routes to the target tunnel are configured. This change does not affect remotely managed tunnels, but for locally managed tunnels, users that might be relying on this feature flag to block traffic should instead guarantee that tunnel has no Private Routes configured for the tunnel.
|
||||||
|
## 2023.7.0
|
||||||
|
### New Features
|
||||||
|
- You can now enable additional diagnostics over the management.argotunnel.com service for your active cloudflared connectors via a new runtime flag `--management-diagnostics` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`). This feature is provided as opt-in and requires the flag to enable. Endpoints such as /metrics provides your prometheus metrics endpoint another mechanism to be reached. Additionally /debug/pprof/(goroutine|heap) are also introduced to allow for remotely retrieving active pprof information from a running cloudflared connector.
|
||||||
|
|
||||||
## 2023.4.1
|
## 2023.4.1
|
||||||
### New Features
|
### 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.
|
- 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.
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
# use a builder image for building cloudflare
|
# use a builder image for building cloudflare
|
||||||
ARG TARGET_GOOS
|
ARG TARGET_GOOS
|
||||||
ARG TARGET_GOARCH
|
ARG TARGET_GOARCH
|
||||||
FROM golang:1.19 as builder
|
FROM golang:1.21.5 as builder
|
||||||
ENV GO111MODULE=on \
|
ENV GO111MODULE=on \
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
TARGET_GOOS=${TARGET_GOOS} \
|
TARGET_GOOS=${TARGET_GOOS} \
|
||||||
TARGET_GOARCH=${TARGET_GOARCH}
|
TARGET_GOARCH=${TARGET_GOARCH}
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||||
|
|
||||||
# copy our sources into the builder image
|
# copy our sources into the builder image
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN .teamcity/install-cloudflare-go.sh
|
||||||
|
|
||||||
# compile cloudflared
|
# compile cloudflared
|
||||||
RUN make cloudflared
|
RUN PATH="/tmp/go/bin:$PATH" make cloudflared
|
||||||
|
|
||||||
# use a distroless base image with glibc
|
# use a distroless base image with glibc
|
||||||
FROM gcr.io/distroless/base-debian11:nonroot
|
FROM gcr.io/distroless/base-debian11:nonroot
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# use a builder image for building cloudflare
|
# use a builder image for building cloudflare
|
||||||
FROM golang:1.19 as builder
|
FROM golang:1.21.5 as builder
|
||||||
ENV GO111MODULE=on \
|
ENV GO111MODULE=on \
|
||||||
CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
|
|
||||||
|
@ -8,8 +8,10 @@ WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||||
# copy our sources into the builder image
|
# copy our sources into the builder image
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN .teamcity/install-cloudflare-go.sh
|
||||||
|
|
||||||
# compile cloudflared
|
# compile cloudflared
|
||||||
RUN GOOS=linux GOARCH=amd64 make cloudflared
|
RUN GOOS=linux GOARCH=amd64 PATH="/tmp/go/bin:$PATH" make cloudflared
|
||||||
|
|
||||||
# use a distroless base image with glibc
|
# use a distroless base image with glibc
|
||||||
FROM gcr.io/distroless/base-debian11:nonroot
|
FROM gcr.io/distroless/base-debian11:nonroot
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# use a builder image for building cloudflare
|
# use a builder image for building cloudflare
|
||||||
FROM golang:1.19 as builder
|
FROM golang:1.21.5 as builder
|
||||||
ENV GO111MODULE=on \
|
ENV GO111MODULE=on \
|
||||||
CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
|
|
||||||
|
@ -8,8 +8,10 @@ WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||||
# copy our sources into the builder image
|
# copy our sources into the builder image
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN .teamcity/install-cloudflare-go.sh
|
||||||
|
|
||||||
# compile cloudflared
|
# compile cloudflared
|
||||||
RUN GOOS=linux GOARCH=arm64 make cloudflared
|
RUN GOOS=linux GOARCH=arm64 PATH="/tmp/go/bin:$PATH" make cloudflared
|
||||||
|
|
||||||
# use a distroless base image with glibc
|
# use a distroless base image with glibc
|
||||||
FROM gcr.io/distroless/base-debian11:nonroot-arm64
|
FROM gcr.io/distroless/base-debian11:nonroot-arm64
|
||||||
|
|
93
Makefile
93
Makefile
|
@ -1,3 +1,6 @@
|
||||||
|
# The targets cannot be run in parallel
|
||||||
|
.NOTPARALLEL:
|
||||||
|
|
||||||
VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||||
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
|
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
|
||||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||||
|
@ -49,6 +52,8 @@ PACKAGE_DIR := $(CURDIR)/packaging
|
||||||
PREFIX := /usr
|
PREFIX := /usr
|
||||||
INSTALL_BINDIR := $(PREFIX)/bin/
|
INSTALL_BINDIR := $(PREFIX)/bin/
|
||||||
INSTALL_MANDIR := $(PREFIX)/share/man/man1/
|
INSTALL_MANDIR := $(PREFIX)/share/man/man1/
|
||||||
|
CF_GO_PATH := /tmp/go
|
||||||
|
PATH := $(CF_GO_PATH)/bin:$(PATH)
|
||||||
|
|
||||||
LOCAL_ARCH ?= $(shell uname -m)
|
LOCAL_ARCH ?= $(shell uname -m)
|
||||||
ifneq ($(GOARCH),)
|
ifneq ($(GOARCH),)
|
||||||
|
@ -126,7 +131,7 @@ ifeq ($(FIPS), true)
|
||||||
$(info Building cloudflared with go-fips)
|
$(info Building cloudflared with go-fips)
|
||||||
cp -f fips/fips.go.linux-amd64 cmd/cloudflared/fips.go
|
cp -f fips/fips.go.linux-amd64 cmd/cloudflared/fips.go
|
||||||
endif
|
endif
|
||||||
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -v -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||||
ifeq ($(FIPS), true)
|
ifeq ($(FIPS), true)
|
||||||
rm -f cmd/cloudflared/fips.go
|
rm -f cmd/cloudflared/fips.go
|
||||||
./check-fips.sh cloudflared
|
./check-fips.sh cloudflared
|
||||||
|
@ -140,6 +145,7 @@ container:
|
||||||
generate-docker-version:
|
generate-docker-version:
|
||||||
echo latest $(VERSION) > versions
|
echo latest $(VERSION) > versions
|
||||||
|
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: vet
|
test: vet
|
||||||
ifndef CI
|
ifndef CI
|
||||||
|
@ -147,17 +153,35 @@ ifndef CI
|
||||||
else
|
else
|
||||||
@mkdir -p .cover
|
@mkdir -p .cover
|
||||||
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
|
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
|
||||||
go tool cover -html ".cover/c.out" -o .cover/all.html
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
.PHONY: cover
|
||||||
|
cover:
|
||||||
|
@echo ""
|
||||||
|
@echo "=====> Total test coverage: <====="
|
||||||
|
@echo ""
|
||||||
|
# Print the overall coverage here for quick access.
|
||||||
|
$Q go tool cover -func ".cover/c.out" | grep "total:" | awk '{print $$3}'
|
||||||
|
# Generate the HTML report that can be viewed from the browser in CI.
|
||||||
|
$Q go tool cover -html ".cover/c.out" -o .cover/all.html
|
||||||
|
|
||||||
.PHONY: test-ssh-server
|
.PHONY: test-ssh-server
|
||||||
test-ssh-server:
|
test-ssh-server:
|
||||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||||
|
|
||||||
|
.PHONY: install-go
|
||||||
|
install-go:
|
||||||
|
rm -rf ${CF_GO_PATH}
|
||||||
|
./.teamcity/install-cloudflare-go.sh
|
||||||
|
|
||||||
|
.PHONY: cleanup-go
|
||||||
|
cleanup-go:
|
||||||
|
rm -rf ${CF_GO_PATH}
|
||||||
|
|
||||||
cloudflared.1: cloudflared_man_template
|
cloudflared.1: cloudflared_man_template
|
||||||
sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' cloudflared_man_template > cloudflared.1
|
sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' cloudflared_man_template > cloudflared.1
|
||||||
|
|
||||||
install: cloudflared cloudflared.1
|
install: install-go cloudflared cloudflared.1 cleanup-go
|
||||||
mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR)
|
mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR)
|
||||||
install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared
|
install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared
|
||||||
install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1
|
install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1
|
||||||
|
@ -199,67 +223,6 @@ cloudflared-darwin-amd64.tgz: cloudflared
|
||||||
tar czf cloudflared-darwin-amd64.tgz cloudflared
|
tar czf cloudflared-darwin-amd64.tgz cloudflared
|
||||||
rm cloudflared
|
rm cloudflared
|
||||||
|
|
||||||
.PHONY: cloudflared-junos
|
|
||||||
cloudflared-junos: cloudflared jetez-certificate.pem jetez-key.pem
|
|
||||||
jetez --source . \
|
|
||||||
-j jet.yaml \
|
|
||||||
--key jetez-key.pem \
|
|
||||||
--cert jetez-certificate.pem \
|
|
||||||
--version $(VERSION)
|
|
||||||
rm jetez-*.pem
|
|
||||||
|
|
||||||
jetez-certificate.pem:
|
|
||||||
ifndef JETEZ_CERT
|
|
||||||
$(error JETEZ_CERT not defined)
|
|
||||||
endif
|
|
||||||
@echo "Writing JetEZ certificate"
|
|
||||||
@echo "$$JETEZ_CERT" > jetez-certificate.pem
|
|
||||||
|
|
||||||
jetez-key.pem:
|
|
||||||
ifndef JETEZ_KEY
|
|
||||||
$(error JETEZ_KEY not defined)
|
|
||||||
endif
|
|
||||||
@echo "Writing JetEZ key"
|
|
||||||
@echo "$$JETEZ_KEY" > jetez-key.pem
|
|
||||||
|
|
||||||
.PHONY: publish-cloudflared-junos
|
|
||||||
publish-cloudflared-junos: cloudflared-junos cloudflared-x86-64.latest.s3
|
|
||||||
ifndef S3_ENDPOINT
|
|
||||||
$(error S3_HOST not defined)
|
|
||||||
endif
|
|
||||||
ifndef S3_URI
|
|
||||||
$(error S3_URI not defined)
|
|
||||||
endif
|
|
||||||
ifndef S3_ACCESS_KEY
|
|
||||||
$(error S3_ACCESS_KEY not defined)
|
|
||||||
endif
|
|
||||||
ifndef S3_SECRET_KEY
|
|
||||||
$(error S3_SECRET_KEY not defined)
|
|
||||||
endif
|
|
||||||
sha256sum cloudflared-x86-64-$(VERSION).tgz | awk '{printf $$1}' > cloudflared-x86-64-$(VERSION).tgz.shasum
|
|
||||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
|
||||||
put cloudflared-x86-64-$(VERSION).tgz $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz
|
|
||||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
|
||||||
put cloudflared-x86-64-$(VERSION).tgz.shasum $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz.shasum
|
|
||||||
dpkg --compare-versions "$(VERSION)" gt "$(shell cat cloudflared-x86-64.latest.s3)" && \
|
|
||||||
echo -n "$(VERSION)" > cloudflared-x86-64.latest && \
|
|
||||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
|
|
||||||
put cloudflared-x86-64.latest $(S3_URI)/cloudflared-x86-64.latest || \
|
|
||||||
echo "Latest version not updated"
|
|
||||||
|
|
||||||
cloudflared-x86-64.latest.s3:
|
|
||||||
s4cmd --endpoint-url $(S3_ENDPOINT) --force \
|
|
||||||
get $(S3_URI)/cloudflared-x86-64.latest cloudflared-x86-64.latest.s3
|
|
||||||
|
|
||||||
.PHONY: homebrew-upload
|
|
||||||
homebrew-upload: cloudflared-darwin-amd64.tgz
|
|
||||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $$^ $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz
|
|
||||||
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz $(S3_URI)/cloudflared-stable-$1.tgz
|
|
||||||
|
|
||||||
.PHONY: homebrew-release
|
|
||||||
homebrew-release: homebrew-upload
|
|
||||||
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
|
|
||||||
|
|
||||||
.PHONY: github-release
|
.PHONY: github-release
|
||||||
github-release: cloudflared
|
github-release: cloudflared
|
||||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||||
|
@ -302,7 +265,7 @@ quic-deps:
|
||||||
|
|
||||||
.PHONY: vet
|
.PHONY: vet
|
||||||
vet:
|
vet:
|
||||||
go vet -v -mod=vendor github.com/cloudflare/cloudflared/...
|
go vet -mod=vendor github.com/cloudflare/cloudflared/...
|
||||||
|
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt:
|
fmt:
|
||||||
|
|
|
@ -31,7 +31,7 @@ Downloads are available as standalone binaries, a Docker image, and Debian, RPM,
|
||||||
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux)
|
* 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)
|
* 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)
|
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
|
||||||
* Build from source with the [instructions here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#build-from-source)
|
* To build from source, first you need to download the go toolchain by running `./.teamcity/install-cloudflare-go.sh` and follow the output. Then you can run `make cloudflared`
|
||||||
|
|
||||||
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||||
|
|
||||||
|
@ -53,9 +53,6 @@ Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do
|
||||||
|
|
||||||
## Deprecated versions
|
## Deprecated versions
|
||||||
|
|
||||||
Cloudflare currently supports versions of `cloudflared` 2020.5.1 and later. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
Cloudflare currently supports versions of cloudflared that are **within one year** of the most recent release. Breaking changes unrelated to feature availability may be introduced that will impact versions released more than one year ago. You can read more about upgrading cloudflared in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/#updating-cloudflared).
|
||||||
|
|
||||||
| Version(s) | Deprecation status |
|
For example, as of January 2023 Cloudflare will support cloudflared version 2023.1.1 to cloudflared 2022.1.1.
|
||||||
|---|---|
|
|
||||||
| 2020.5.1 and later | Supported |
|
|
||||||
| Versions prior to 2020.5.1 | No longer supported |
|
|
||||||
|
|
150
RELEASE_NOTES
150
RELEASE_NOTES
|
@ -1,3 +1,153 @@
|
||||||
|
2024.4.1
|
||||||
|
- 2024-04-22 TUN-8380: Add sleep before requesting quick tunnel as temporary fix for component tests
|
||||||
|
- 2024-04-19 TUN-8374: Close UDP socket if registration fails
|
||||||
|
- 2024-04-18 TUN-8371: Bump quic-go to v0.42.0
|
||||||
|
- 2024-04-03 TUN-8333: Bump go-jose dependency to v4
|
||||||
|
- 2024-04-02 TUN-8331: Add unit testing for AccessJWTValidator middleware
|
||||||
|
|
||||||
|
2024.4.0
|
||||||
|
- 2024-04-02 feat: provide short version (#1206)
|
||||||
|
- 2024-04-02 Format code
|
||||||
|
- 2024-01-18 feat: auto tls sni
|
||||||
|
- 2023-12-24 fix checkInPingGroup bugs
|
||||||
|
- 2023-12-15 Add environment variables for TCP tunnel hostname / destination / URL.
|
||||||
|
|
||||||
|
2024.3.0
|
||||||
|
- 2024-03-14 TUN-8281: Run cloudflared query list tunnels/routes endpoint in a paginated way
|
||||||
|
- 2024-03-13 TUN-8297: Improve write timeout logging on safe_stream.go
|
||||||
|
- 2024-03-07 TUN-8290: Remove `|| true` from postrm.sh
|
||||||
|
- 2024-03-05 TUN-8275: Skip write timeout log on "no network activity"
|
||||||
|
- 2024-01-23 Update postrm.sh to fix incomplete uninstall
|
||||||
|
- 2024-01-05 fix typo in errcheck for response parsing logic in CreateTunnel routine
|
||||||
|
- 2023-12-23 Update linux_service.go
|
||||||
|
- 2023-12-07 ci: bump actions/checkout to v4
|
||||||
|
- 2023-12-07 ci/check: bump actions/setup-go to v5
|
||||||
|
- 2023-04-28 check.yaml: bump actions/setup-go to v4
|
||||||
|
|
||||||
|
2024.2.1
|
||||||
|
- 2024-02-20 TUN-8242: Update Changes.md file with new remote diagnostics behaviour
|
||||||
|
- 2024-02-19 TUN-8238: Fix type mismatch introduced by fast-forward
|
||||||
|
- 2024-02-16 TUN-8243: Collect metrics on the number of QUIC frames sent/received
|
||||||
|
- 2024-02-15 TUN-8238: Refactor proxy logging
|
||||||
|
- 2024-02-14 TUN-8242: Enable remote diagnostics by default
|
||||||
|
- 2024-02-12 TUN-8236: Add write timeout to quic and tcp connections
|
||||||
|
- 2024-02-09 TUN-8224: Fix safety of TCP stream logging, separate connect and ack log messages
|
||||||
|
|
||||||
|
2024.2.0
|
||||||
|
- 2024-02-07 TUN-8224: Count and collect metrics on stream connect successes/errors
|
||||||
|
|
||||||
|
2024.1.5
|
||||||
|
- 2024-01-22 TUN-8176: Support ARM platforms that don't have an FPU or have it enabled in kernel
|
||||||
|
- 2024-01-15 TUN-8158: Bring back commit e6537418859afcac29e56a39daa08bcabc09e048 and fixes infinite loop on linux when the socket is closed
|
||||||
|
|
||||||
|
2024.1.4
|
||||||
|
- 2024-01-19 Revert "TUN-8158: Add logging to confirm when ICMP reply is returned to the edge"
|
||||||
|
|
||||||
|
2024.1.3
|
||||||
|
- 2024-01-15 TUN-8161: Fix broken ARM build for armv6
|
||||||
|
- 2024-01-15 TUN-8158: Add logging to confirm when ICMP reply is returned to the edge
|
||||||
|
|
||||||
|
2024.1.2
|
||||||
|
- 2024-01-11 TUN-8147: Disable ECN usage due to bugs in detecting if supported
|
||||||
|
- 2024-01-11 TUN-8146: Fix export path for install-go command
|
||||||
|
- 2024-01-11 TUN-8146: Fix Makefile targets should not be run in parallel and install-go script was missing shebang
|
||||||
|
- 2024-01-10 TUN-8140: Remove homebrew scripts
|
||||||
|
|
||||||
|
2024.1.1
|
||||||
|
- 2024-01-10 TUN-8134: Revert installed prefix to /usr
|
||||||
|
- 2024-01-09 TUN-8130: Fix path to install go for mac build
|
||||||
|
- 2024-01-09 TUN-8129: Use the same build command between branch and release builds
|
||||||
|
- 2024-01-09 TUN-8130: Install go tool chain in /tmp on build agents
|
||||||
|
- 2024-01-09 TUN-8134: Install cloudflare go as part of make install
|
||||||
|
- 2024-01-08 TUN-8118: Disable FIPS module to build with go-boring without CGO_ENABLED
|
||||||
|
|
||||||
|
2024.1.0
|
||||||
|
- 2024-01-01 TUN-7934: Update quic-go to a version that queues datagrams for better throughput and drops large datagram
|
||||||
|
- 2023-12-20 TUN-8072: Need to set GOCACHE in mac go installation script
|
||||||
|
- 2023-12-17 TUN-8072: Add script to download cloudflare go for Mac build agents
|
||||||
|
- 2023-12-15 Fix nil pointer dereference segfault when passing "null" config json to cloudflared tunnel ingress validate (#1070)
|
||||||
|
- 2023-12-15 configuration.go: fix developerPortal link (#960)
|
||||||
|
- 2023-12-14 tunnelrpc/pogs: fix dropped test errors (#1106)
|
||||||
|
- 2023-12-14 cmd/cloudflared/updater: fix dropped error (#1055)
|
||||||
|
- 2023-12-14 use os.Executable to discover the path to cloudflared (#1040)
|
||||||
|
- 2023-12-14 Remove extraneous `period` from Path Environment Variable (#1009)
|
||||||
|
- 2023-12-14 Use CLI context when running tunnel (#597)
|
||||||
|
- 2023-12-14 TUN-8066: Define scripts to build on Windows agents
|
||||||
|
- 2023-12-11 TUN-8052: Update go to 1.21.5
|
||||||
|
- 2023-12-07 TUN-7970: Default to enable post quantum encryption for quic transport
|
||||||
|
- 2023-12-04 TUN-8006: Update quic-go to latest upstream
|
||||||
|
- 2023-11-15 VULN-44842 Add a flag that allows users to not send the Access JWT to stdout
|
||||||
|
- 2023-11-13 TUN-7965: Remove legacy incident status page check
|
||||||
|
- 2023-11-13 AUTH-5682 Org token flow in Access logins should pass CF_AppSession cookie
|
||||||
|
|
||||||
|
2023.10.0
|
||||||
|
- 2023-10-06 TUN-7864: Document cloudflared versions support
|
||||||
|
- 2023-10-03 CUSTESC-33731: Make rule match test report rule in 0-index base
|
||||||
|
- 2023-09-22 TUN-7824: Fix usage of systemctl status to detect which services are installed
|
||||||
|
- 2023-09-20 TUN-7813: Improve tunnel delete command to use cascade delete
|
||||||
|
- 2023-09-20 TUN-7787: cloudflared only list ip routes targeted for cfd_tunnel
|
||||||
|
- 2023-09-15 TUN-7787: Refactor cloudflared to use new route endpoints based on route IDs
|
||||||
|
- 2023-09-08 TUN-7776: Remove warp-routing flag from cloudflared
|
||||||
|
- 2023-09-05 TUN-7756: Clarify that QUIC is mandatory to support ICMP proxying
|
||||||
|
|
||||||
|
2023.8.2
|
||||||
|
- 2023-08-25 TUN-7700: Implement feature selector to determine if connections will prefer post quantum cryptography
|
||||||
|
- 2023-08-22 TUN-7707: Use X25519Kyber768Draft00 curve when post-quantum feature is enabled
|
||||||
|
|
||||||
|
2023.8.1
|
||||||
|
- 2023-08-23 TUN-7718: Update R2 Token to no longer encode secret
|
||||||
|
|
||||||
|
2023.8.0
|
||||||
|
- 2023-07-26 TUN-7584: Bump go 1.20.6
|
||||||
|
|
||||||
|
2023.7.3
|
||||||
|
- 2023-07-25 TUN-7628: Correct Host parsing for Access
|
||||||
|
- 2023-07-24 TUN-7624: Fix flaky TestBackoffGracePeriod test in cloudflared
|
||||||
|
|
||||||
|
2023.7.2
|
||||||
|
- 2023-07-19 TUN-7599: Onboard cloudflared to Software Dashboard
|
||||||
|
- 2023-07-19 TUN-7587: Remove junos builds
|
||||||
|
- 2023-07-18 TUN-7597: Add flag to disable auto-update services to be installed
|
||||||
|
- 2023-07-17 TUN-7594: Add nightly arm64 cloudflared internal deb publishes
|
||||||
|
- 2023-07-14 TUN-7586: Upgrade go-jose/go-jose/v3 and core-os/go-oidc/v3
|
||||||
|
- 2023-07-14 TUN-7589: Remove legacy golang.org/x/crypto/ssh/terminal package usage
|
||||||
|
- 2023-07-14 TUN-7590: Remove usages of ioutil
|
||||||
|
- 2023-07-14 TUN-7585: Remove h2mux compression
|
||||||
|
- 2023-07-14 TUN-7588: Update package coreos/go-systemd
|
||||||
|
|
||||||
|
2023.7.1
|
||||||
|
- 2023-07-13 TUN-7582: Correct changelog wording for --management-diagnostics
|
||||||
|
- 2023-07-12 TUN-7575: Add option to disable PTMU discovery over QUIC
|
||||||
|
|
||||||
|
2023.7.0
|
||||||
|
- 2023-07-06 TUN-7558: Flush on Writes for StreamBasedOriginProxy
|
||||||
|
- 2023-07-05 TUN-7553: Add flag to enable management diagnostic services
|
||||||
|
- 2023-07-05 TUN-7564: Support cf-trace-id for cloudflared access
|
||||||
|
- 2023-07-05 TUN-7477: Decrement UDP sessions on shutdown
|
||||||
|
- 2023-07-03 TUN-7545: Add support for full bidirectionally streaming with close signal propagation
|
||||||
|
- 2023-06-30 TUN-7549: Add metrics route to management service
|
||||||
|
- 2023-06-30 TUN-7551: Complete removal of raven-go to sentry-go
|
||||||
|
- 2023-06-30 TUN-7550: Add pprof endpoint to management service
|
||||||
|
- 2023-06-29 TUN-7543: Add --debug-stream flag to cloudflared access ssh
|
||||||
|
- 2023-06-26 TUN-6011: Remove docker networks from ICMP Proxy test
|
||||||
|
- 2023-06-20 AUTH-5328 Pass cloudflared_token_check param when running cloudflared access login
|
||||||
|
|
||||||
|
2023.6.1
|
||||||
|
- 2023-06-19 TUN-7480: Added a timeout for unregisterUDP.
|
||||||
|
- 2023-06-16 TUN-7477: Add UDP/TCP session metrics
|
||||||
|
- 2023-06-14 TUN-7468: Increase the limit of incoming streams
|
||||||
|
|
||||||
|
2023.6.0
|
||||||
|
- 2023-06-15 TUN-7471: Fixes cloudflared not closing the quic stream on unregister UDP session
|
||||||
|
- 2023-06-09 TUN-7463: Add default ingress rule if no ingress rules are provided when updating the configuration
|
||||||
|
- 2023-05-31 TUN-7447: Add a cover build to report code coverage
|
||||||
|
|
||||||
|
2023.5.1
|
||||||
|
- 2023-05-16 TUN-7424: Add CORS headers to host_details responses
|
||||||
|
- 2023-05-11 TUN-7421: Add *.cloudflare.com to permitted Origins for management WebSocket requests
|
||||||
|
- 2023-05-05 TUN-7404: Default configuration version set to -1
|
||||||
|
- 2023-05-05 TUN-7227: Migrate to devincarr/quic-go
|
||||||
|
|
||||||
2023.5.0
|
2023.5.0
|
||||||
- 2023-04-27 TUN-7398: Add support for quic safe stream to set deadline
|
- 2023-04-27 TUN-7398: Add support for quic safe stream to set deadline
|
||||||
- 2023-04-26 TUN-7394: Retry StartFirstTunnel on quic.ApplicationErrors
|
- 2023-04-26 TUN-7394: Retry StartFirstTunnel on quic.ApplicationErrors
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||||
echo $VERSION
|
echo $VERSION
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||||
echo $VERSION
|
echo $VERSION
|
||||||
|
|
||||||
# Avoid depending on C code since we don't need it.
|
# Disable FIPS module in go-boring
|
||||||
|
export GOEXPERIMENT=noboringcrypto
|
||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
|
|
||||||
# This controls the directory the built artifacts go into
|
# This controls the directory the built artifacts go into
|
||||||
|
@ -13,6 +15,12 @@ export TARGET_OS=linux
|
||||||
for arch in ${linuxArchs[@]}; do
|
for arch in ${linuxArchs[@]}; do
|
||||||
unset TARGET_ARM
|
unset TARGET_ARM
|
||||||
export TARGET_ARCH=$arch
|
export TARGET_ARCH=$arch
|
||||||
|
|
||||||
|
## Support for arm platforms without hardware FPU enabled
|
||||||
|
if [[ $arch == arm ]] ; then
|
||||||
|
export TARGET_ARCH=arm
|
||||||
|
export TARGET_ARM=5
|
||||||
|
fi
|
||||||
|
|
||||||
## Support for armhf builds
|
## Support for armhf builds
|
||||||
if [[ $arch == armhf ]] ; then
|
if [[ $arch == armhf ]] ; then
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
apiVersion: backstage.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: cloudflared
|
||||||
|
description: Client for Cloudflare Tunnels
|
||||||
|
annotations:
|
||||||
|
backstage.io/source-location: url:https://bitbucket.cfdata.org/projects/TUN/repos/cloudflared/browse
|
||||||
|
cloudflare.com/software-excellence-opt-in: "true"
|
||||||
|
cloudflare.com/jira-project-key: "TUN"
|
||||||
|
cloudflare.com/jira-project-component: "Cloudflare Tunnel"
|
||||||
|
tags:
|
||||||
|
- internal
|
||||||
|
spec:
|
||||||
|
type: "service"
|
||||||
|
lifecycle: "Active"
|
||||||
|
owner: "teams/tunnel-teams-routing"
|
|
@ -109,20 +109,34 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (
|
||||||
return r.client.Do(req)
|
return r.client.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseResponse(reader io.Reader, data interface{}) error {
|
func parseResponseEnvelope(reader io.Reader) (*response, error) {
|
||||||
// Schema for Tunnelstore responses in the v1 API.
|
// Schema for Tunnelstore responses in the v1 API.
|
||||||
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
||||||
var result response
|
var result response
|
||||||
// First, parse the wrapper and check the API call succeeded
|
// First, parse the wrapper and check the API call succeeded
|
||||||
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
||||||
return errors.Wrap(err, "failed to decode response")
|
return nil, errors.Wrap(err, "failed to decode response")
|
||||||
}
|
}
|
||||||
if err := result.checkErrors(); err != nil {
|
if err := result.checkErrors(); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !result.Success {
|
if !result.Success {
|
||||||
return ErrAPINoSuccess
|
return nil, ErrAPINoSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponse(reader io.Reader, data interface{}) error {
|
||||||
|
result, err := parseResponseEnvelope(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseResponseBody(result, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResponseBody(result *response, data interface{}) error {
|
||||||
// At this point we know the API call succeeded, so, parse out the inner
|
// At this point we know the API call succeeded, so, parse out the inner
|
||||||
// result into the datatype provided as a parameter.
|
// result into the datatype provided as a parameter.
|
||||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||||
|
@ -131,11 +145,58 @@ func parseResponse(reader io.Reader, data interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T, error) {
|
||||||
|
page := 0
|
||||||
|
var fullResponse []*T
|
||||||
|
|
||||||
|
for {
|
||||||
|
page += 1
|
||||||
|
envelope, parsedBody, err := fetchPage[T](requestFn, page)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, fmt.Sprintf("Error Parsing page %d", page))
|
||||||
|
}
|
||||||
|
|
||||||
|
fullResponse = append(fullResponse, parsedBody...)
|
||||||
|
if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return fullResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*response, []*T, error) {
|
||||||
|
pageResp, err := requestFn(page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
defer pageResp.Body.Close()
|
||||||
|
if pageResp.StatusCode == http.StatusOK {
|
||||||
|
envelope, err := parseResponseEnvelope(pageResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var parsedRspBody []*T
|
||||||
|
return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
type response struct {
|
type response struct {
|
||||||
Success bool `json:"success,omitempty"`
|
Success bool `json:"success,omitempty"`
|
||||||
Errors []apiErr `json:"errors,omitempty"`
|
Errors []apiErr `json:"errors,omitempty"`
|
||||||
Messages []string `json:"messages,omitempty"`
|
Messages []string `json:"messages,omitempty"`
|
||||||
Result json.RawMessage `json:"result,omitempty"`
|
Result json.RawMessage `json:"result,omitempty"`
|
||||||
|
Pagination Pagination `json:"result_info,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pagination struct {
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Page int `json:"page,omitempty"`
|
||||||
|
PerPage int `json:"per_page,omitempty"`
|
||||||
|
TotalCount int `json:"total_count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *response) checkErrors() error {
|
func (r *response) checkErrors() error {
|
||||||
|
|
|
@ -9,7 +9,7 @@ type TunnelClient interface {
|
||||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||||
GetTunnelToken(tunnelID uuid.UUID) (string, error)
|
GetTunnelToken(tunnelID uuid.UUID) (string, error)
|
||||||
GetManagementToken(tunnelID uuid.UUID) (string, error)
|
GetManagementToken(tunnelID uuid.UUID) (string, error)
|
||||||
DeleteTunnel(tunnelID uuid.UUID) error
|
DeleteTunnel(tunnelID uuid.UUID, cascade bool) error
|
||||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||||
|
@ -22,7 +22,7 @@ type HostnameClient interface {
|
||||||
type IPRouteClient interface {
|
type IPRouteClient interface {
|
||||||
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
||||||
AddRoute(newRoute NewRoute) (Route, error)
|
AddRoute(newRoute NewRoute) (Route, error)
|
||||||
DeleteRoute(params DeleteRouteParams) error
|
DeleteRoute(id uuid.UUID) error
|
||||||
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,10 +75,12 @@ type NewRoute struct {
|
||||||
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||||
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
|
Network string `json:"network"`
|
||||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||||
}{
|
}{
|
||||||
|
Network: r.Network.String(),
|
||||||
TunnelID: r.TunnelID,
|
TunnelID: r.TunnelID,
|
||||||
Comment: r.Comment,
|
Comment: r.Comment,
|
||||||
VNetID: r.VNetID,
|
VNetID: r.VNetID,
|
||||||
|
@ -87,6 +89,7 @@ func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
||||||
type DetailedRoute struct {
|
type DetailedRoute struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
Network CIDR `json:"network"`
|
Network CIDR `json:"network"`
|
||||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||||
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
|
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
|
||||||
|
@ -115,7 +118,8 @@ func (r DetailedRoute) TableString() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||||
|
r.ID,
|
||||||
r.Network.String(),
|
r.Network.String(),
|
||||||
vnetColumn,
|
vnetColumn,
|
||||||
r.Comment,
|
r.Comment,
|
||||||
|
@ -126,12 +130,6 @@ func (r DetailedRoute) TableString() string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteRouteParams struct {
|
|
||||||
Network net.IPNet
|
|
||||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
|
||||||
VNetID *uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetRouteByIpParams struct {
|
type GetRouteByIpParams struct {
|
||||||
Ip net.IP
|
Ip net.IP
|
||||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||||
|
@ -139,26 +137,30 @@ type GetRouteByIpParams struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||||
|
// Due to pagination on the server side it will call the endpoint multiple times if needed.
|
||||||
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
fetchFn := func(page int) (*http.Response, error) {
|
||||||
endpoint.RawQuery = filter.Encode()
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
filter.Page(page)
|
||||||
if err != nil {
|
endpoint.RawQuery = filter.Encode()
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
rsp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
if err != nil {
|
||||||
return parseListDetailedRoutes(resp.Body)
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
|
if rsp.StatusCode != http.StatusOK {
|
||||||
|
rsp.Body.Close()
|
||||||
|
return nil, r.statusCodeToError("list routes", rsp)
|
||||||
|
}
|
||||||
|
return rsp, nil
|
||||||
}
|
}
|
||||||
|
return fetchExhaustively[DetailedRoute](fetchFn)
|
||||||
return nil, r.statusCodeToError("list routes", resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||||
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
endpoint.Path = path.Join(endpoint.Path)
|
||||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Route{}, errors.Wrap(err, "REST request failed")
|
return Route{}, errors.Wrap(err, "REST request failed")
|
||||||
|
@ -173,10 +175,9 @@ func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||||
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
|
func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||||
setVnetParam(&endpoint, params.VNetID)
|
|
||||||
|
|
||||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -211,12 +212,6 @@ func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
||||||
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
|
||||||
var routes []*DetailedRoute
|
|
||||||
err := parseResponse(body, &routes)
|
|
||||||
return routes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRoute(body io.ReadCloser) (Route, error) {
|
func parseRoute(body io.ReadCloser) (Route, error) {
|
||||||
var route Route
|
var route Route
|
||||||
err := parseResponse(body, &route)
|
err := parseResponse(body, &route)
|
||||||
|
|
|
@ -58,31 +58,29 @@ type IpRouteFilter struct {
|
||||||
|
|
||||||
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
||||||
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||||
f := &IpRouteFilter{
|
f := NewIPRouteFilter()
|
||||||
queryParams: url.Values{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set deletion filter
|
// Set deletion filter
|
||||||
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||||
f.deleted()
|
f.Deleted()
|
||||||
} else {
|
} else {
|
||||||
f.notDeleted()
|
f.NotDeleted()
|
||||||
}
|
}
|
||||||
|
|
||||||
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if subset != nil {
|
} else if subset != nil {
|
||||||
f.networkIsSupersetOf(*subset)
|
f.NetworkIsSupersetOf(*subset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if superset != nil {
|
} else if superset != nil {
|
||||||
f.networkIsSupersetOf(*superset)
|
f.NetworkIsSupersetOf(*superset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
||||||
f.commentIs(comment)
|
f.CommentIs(comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
||||||
|
@ -90,7 +88,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
||||||
}
|
}
|
||||||
f.tunnelID(u)
|
f.TunnelID(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
||||||
|
@ -98,7 +96,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
||||||
}
|
}
|
||||||
f.vnetID(u)
|
f.VNetID(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||||
|
@ -124,35 +122,44 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||||
return subset, nil
|
return subset, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) commentIs(comment string) {
|
func NewIPRouteFilter() *IpRouteFilter {
|
||||||
|
values := &IpRouteFilter{queryParams: url.Values{}}
|
||||||
|
|
||||||
|
// always list cfd_tunnel routes only
|
||||||
|
values.queryParams.Set("tun_types", "cfd_tunnel")
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *IpRouteFilter) CommentIs(comment string) {
|
||||||
f.queryParams.Set("comment", comment)
|
f.queryParams.Set("comment", comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) notDeleted() {
|
func (f *IpRouteFilter) NotDeleted() {
|
||||||
f.queryParams.Set("is_deleted", "false")
|
f.queryParams.Set("is_deleted", "false")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) deleted() {
|
func (f *IpRouteFilter) Deleted() {
|
||||||
f.queryParams.Set("is_deleted", "true")
|
f.queryParams.Set("is_deleted", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
|
func (f *IpRouteFilter) NetworkIsSubsetOf(superset net.IPNet) {
|
||||||
f.queryParams.Set("network_subset", superset.String())
|
f.queryParams.Set("network_subset", superset.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
|
func (f *IpRouteFilter) NetworkIsSupersetOf(subset net.IPNet) {
|
||||||
f.queryParams.Set("network_superset", subset.String())
|
f.queryParams.Set("network_superset", subset.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
|
func (f *IpRouteFilter) ExistedAt(existedAt time.Time) {
|
||||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
|
func (f *IpRouteFilter) TunnelID(id uuid.UUID) {
|
||||||
f.queryParams.Set("tunnel_id", id.String())
|
f.queryParams.Set("tunnel_id", id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
|
func (f *IpRouteFilter) VNetID(id uuid.UUID) {
|
||||||
f.queryParams.Set("virtual_network_id", id.String())
|
f.queryParams.Set("virtual_network_id", id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +167,10 @@ func (f *IpRouteFilter) MaxFetchSize(max uint) {
|
||||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *IpRouteFilter) Page(page int) {
|
||||||
|
f.queryParams.Set("page", strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
|
||||||
func (f IpRouteFilter) Encode() string {
|
func (f IpRouteFilter) Encode() string {
|
||||||
return f.queryParams.Encode()
|
return f.queryParams.Encode()
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
`{
|
`{
|
||||||
|
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
|
||||||
"network":"10.1.2.40/29",
|
"network":"10.1.2.40/29",
|
||||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||||
"comment":"test",
|
"comment":"test",
|
||||||
|
@ -80,6 +81,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
`{
|
`{
|
||||||
|
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
|
||||||
"network":"10.1.2.40/29",
|
"network":"10.1.2.40/29",
|
||||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||||
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
|
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
|
||||||
|
@ -167,9 +169,10 @@ func TestRouteTableString(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, network)
|
require.NotNil(t, network)
|
||||||
r := DetailedRoute{
|
r := DetailedRoute{
|
||||||
|
ID: uuid.Nil,
|
||||||
Network: CIDR(*network),
|
Network: CIDR(*network),
|
||||||
}
|
}
|
||||||
row := r.TableString()
|
row := r.TableString()
|
||||||
fmt.Println(row)
|
fmt.Println(row)
|
||||||
require.True(t, strings.HasPrefix(row, "1.2.3.4/32"))
|
require.True(t, strings.HasPrefix(row, "00000000-0000-0000-0000-000000000000\t1.2.3.4/32"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWith
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
var tunnel TunnelWithToken
|
var tunnel TunnelWithToken
|
||||||
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil {
|
if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
|
||||||
return nil, serdeErr
|
return nil, serdeErr
|
||||||
}
|
}
|
||||||
return &tunnel, nil
|
return &tunnel, nil
|
||||||
|
@ -159,9 +159,14 @@ func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err e
|
||||||
return "", r.statusCodeToError("get tunnel token", resp)
|
return "", r.statusCodeToError("get tunnel token", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||||
|
// Cascade will delete all tunnel dependencies (connections, routes, etc.) that
|
||||||
|
// are linked to the deleted tunnel.
|
||||||
|
if cascade {
|
||||||
|
endpoint.RawQuery = "cascade=true"
|
||||||
|
}
|
||||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "REST request failed")
|
return errors.Wrap(err, "REST request failed")
|
||||||
|
@ -172,25 +177,22 @@ func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
||||||
endpoint := r.baseEndpoints.accountLevel
|
fetchFn := func(page int) (*http.Response, error) {
|
||||||
endpoint.RawQuery = filter.encode()
|
endpoint := r.baseEndpoints.accountLevel
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
filter.Page(page)
|
||||||
if err != nil {
|
endpoint.RawQuery = filter.encode()
|
||||||
return nil, errors.Wrap(err, "REST request failed")
|
rsp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
}
|
if err != nil {
|
||||||
defer resp.Body.Close()
|
return nil, errors.Wrap(err, "REST request failed")
|
||||||
|
}
|
||||||
if resp.StatusCode == http.StatusOK {
|
if rsp.StatusCode != http.StatusOK {
|
||||||
return parseListTunnels(resp.Body)
|
rsp.Body.Close()
|
||||||
|
return nil, r.statusCodeToError("list tunnels", rsp)
|
||||||
|
}
|
||||||
|
return rsp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, r.statusCodeToError("list tunnels", resp)
|
return fetchExhaustively[Tunnel](fetchFn)
|
||||||
}
|
|
||||||
|
|
||||||
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
|
||||||
var tunnels []*Tunnel
|
|
||||||
err := parseResponse(body, &tunnels)
|
|
||||||
return tunnels, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||||
|
|
|
@ -50,6 +50,10 @@ func (f *TunnelFilter) MaxFetchSize(max uint) {
|
||||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TunnelFilter) Page(page int) {
|
||||||
|
f.queryParams.Set("page", strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
|
||||||
func (f TunnelFilter) encode() string {
|
func (f TunnelFilter) encode() string {
|
||||||
return f.queryParams.Encode()
|
return f.queryParams.Encode()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package cfapi
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -16,52 +15,6 @@ import (
|
||||||
|
|
||||||
var loc, _ = time.LoadLocation("UTC")
|
var loc, _ = time.LoadLocation("UTC")
|
||||||
|
|
||||||
func Test_parseListTunnels(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
body string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want []*Tunnel
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty list",
|
|
||||||
args: args{body: `{"success": true, "result": []}`},
|
|
||||||
want: []*Tunnel{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success is false",
|
|
||||||
args: args{body: `{"success": false, "result": []}`},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "errors are present",
|
|
||||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid response",
|
|
||||||
args: args{body: `abc`},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
|
||||||
got, err := parseListTunnels(body)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_unmarshalTunnel(t *testing.T) {
|
func Test_unmarshalTunnel(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
body string
|
body string
|
||||||
|
|
93
cfsetup.yaml
93
cfsetup.yaml
|
@ -1,5 +1,4 @@
|
||||||
pinned_go: &pinned_go go=1.19.6-1
|
pinned_go: &pinned_go go-boring=1.21.5-1
|
||||||
pinned_go_fips: &pinned_go_fips go-boring=1.19.6-1
|
|
||||||
|
|
||||||
build_dir: &build_dir /cfsetup_build
|
build_dir: &build_dir /cfsetup_build
|
||||||
default-flavor: bullseye
|
default-flavor: bullseye
|
||||||
|
@ -10,25 +9,36 @@ buster: &buster
|
||||||
- *pinned_go
|
- *pinned_go
|
||||||
- build-essential
|
- build-essential
|
||||||
- gotest-to-teamcity
|
- gotest-to-teamcity
|
||||||
|
- fakeroot
|
||||||
|
- rubygem-fpm
|
||||||
|
- rpm
|
||||||
|
- libffi-dev
|
||||||
|
- reprepro
|
||||||
|
- createrepo
|
||||||
pre-cache: &build_pre_cache
|
pre-cache: &build_pre_cache
|
||||||
- export GOCACHE=/cfsetup_build/.cache/go-build
|
- export GOCACHE=/cfsetup_build/.cache/go-build
|
||||||
- go install golang.org/x/tools/cmd/goimports@latest
|
- go install golang.org/x/tools/cmd/goimports@latest
|
||||||
post-cache:
|
post-cache:
|
||||||
- export GOOS=linux
|
# TODO: TUN-8126 this is temporary to make sure packages can be built before release
|
||||||
- export GOARCH=amd64
|
- ./build-packages.sh
|
||||||
- make cloudflared
|
# Build binary for component test
|
||||||
|
- GOOS=linux GOARCH=amd64 make cloudflared
|
||||||
build-fips:
|
build-fips:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps: &build_deps_fips
|
builddeps: *build_deps
|
||||||
- *pinned_go_fips
|
|
||||||
- build-essential
|
|
||||||
- gotest-to-teamcity
|
|
||||||
pre-cache: *build_pre_cache
|
pre-cache: *build_pre_cache
|
||||||
post-cache:
|
post-cache:
|
||||||
- export GOOS=linux
|
|
||||||
- export GOARCH=amd64
|
|
||||||
- export FIPS=true
|
- export FIPS=true
|
||||||
- make cloudflared
|
# TODO: TUN-8126 this is temporary to make sure packages can be built before release
|
||||||
|
- ./build-packages-fips.sh
|
||||||
|
# Build binary for component test
|
||||||
|
- GOOS=linux GOARCH=amd64 make cloudflared
|
||||||
|
cover:
|
||||||
|
build_dir: *build_dir
|
||||||
|
builddeps: *build_deps
|
||||||
|
pre-cache: *build_pre_cache
|
||||||
|
post-cache:
|
||||||
|
- make cover
|
||||||
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
|
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
|
||||||
github-release-pkgs:
|
github-release-pkgs:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
|
@ -66,7 +76,7 @@ buster: &buster
|
||||||
github-fips-release-pkgs:
|
github-fips-release-pkgs:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps:
|
builddeps:
|
||||||
- *pinned_go_fips
|
- *pinned_go
|
||||||
- build-essential
|
- build-essential
|
||||||
- fakeroot
|
- fakeroot
|
||||||
- rubygem-fpm
|
- rubygem-fpm
|
||||||
|
@ -105,7 +115,7 @@ buster: &buster
|
||||||
build-fips-internal-deb:
|
build-fips-internal-deb:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps: &build_fips_deb_deps
|
builddeps: &build_fips_deb_deps
|
||||||
- *pinned_go_fips
|
- *pinned_go
|
||||||
- build-essential
|
- build-essential
|
||||||
- fakeroot
|
- fakeroot
|
||||||
- rubygem-fpm
|
- rubygem-fpm
|
||||||
|
@ -115,7 +125,7 @@ buster: &buster
|
||||||
- export FIPS=true
|
- export FIPS=true
|
||||||
- export ORIGINAL_NAME=true
|
- export ORIGINAL_NAME=true
|
||||||
- make cloudflared-deb
|
- make cloudflared-deb
|
||||||
build-fips-internal-deb-nightly:
|
build-internal-deb-nightly-amd64:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps: *build_fips_deb_deps
|
builddeps: *build_fips_deb_deps
|
||||||
post-cache:
|
post-cache:
|
||||||
|
@ -125,6 +135,16 @@ buster: &buster
|
||||||
- export FIPS=true
|
- export FIPS=true
|
||||||
- export ORIGINAL_NAME=true
|
- export ORIGINAL_NAME=true
|
||||||
- make cloudflared-deb
|
- make cloudflared-deb
|
||||||
|
build-internal-deb-nightly-arm64:
|
||||||
|
build_dir: *build_dir
|
||||||
|
builddeps: *build_fips_deb_deps
|
||||||
|
post-cache:
|
||||||
|
- export GOOS=linux
|
||||||
|
- export GOARCH=arm64
|
||||||
|
- export NIGHTLY=true
|
||||||
|
#- export FIPS=true # TUN-7595
|
||||||
|
- export ORIGINAL_NAME=true
|
||||||
|
- make cloudflared-deb
|
||||||
build-deb-arm64:
|
build-deb-arm64:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps: *build_deb_deps
|
builddeps: *build_deb_deps
|
||||||
|
@ -179,7 +199,7 @@ buster: &buster
|
||||||
- make test | gotest-to-teamcity
|
- make test | gotest-to-teamcity
|
||||||
test-fips:
|
test-fips:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps: *build_deps_fips
|
builddeps: *build_deps
|
||||||
pre-cache: *build_pre_cache
|
pre-cache: *build_pre_cache
|
||||||
post-cache:
|
post-cache:
|
||||||
- export GOOS=linux
|
- export GOOS=linux
|
||||||
|
@ -211,7 +231,7 @@ buster: &buster
|
||||||
component-test-fips:
|
component-test-fips:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps:
|
builddeps:
|
||||||
- *pinned_go_fips
|
- *pinned_go
|
||||||
- python3.7
|
- python3.7
|
||||||
- python3-pip
|
- python3-pip
|
||||||
- python3-setuptools
|
- python3-setuptools
|
||||||
|
@ -222,51 +242,12 @@ buster: &buster
|
||||||
- component-tests/requirements.txt
|
- component-tests/requirements.txt
|
||||||
pre-cache: *component_test_pre_cache
|
pre-cache: *component_test_pre_cache
|
||||||
post-cache: *component_test_post_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:
|
github-message-release:
|
||||||
build_dir: *build_dir
|
build_dir: *build_dir
|
||||||
builddeps: *build_pygithub
|
builddeps: *build_pygithub
|
||||||
pre-cache: *install_pygithub
|
pre-cache: *install_pygithub
|
||||||
post-cache:
|
post-cache:
|
||||||
- make github-message
|
- make github-message
|
||||||
build-junos:
|
|
||||||
build_dir: *build_dir
|
|
||||||
builddeps:
|
|
||||||
- *pinned_go
|
|
||||||
- build-essential
|
|
||||||
- python3
|
|
||||||
- genisoimage
|
|
||||||
pre-cache:
|
|
||||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
|
||||||
post-cache:
|
|
||||||
- export CGO_ENABLED=0
|
|
||||||
- export GOOS=freebsd
|
|
||||||
- export GOARCH=amd64
|
|
||||||
- make cloudflared
|
|
||||||
publish-junos:
|
|
||||||
build_dir: *build_dir
|
|
||||||
builddeps:
|
|
||||||
- *pinned_go
|
|
||||||
- build-essential
|
|
||||||
- python3
|
|
||||||
- genisoimage
|
|
||||||
- jetez
|
|
||||||
- s4cmd
|
|
||||||
pre-cache:
|
|
||||||
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
|
|
||||||
post-cache:
|
|
||||||
- export GOOS=freebsd
|
|
||||||
- export GOARCH=amd64
|
|
||||||
- make publish-cloudflared-junos
|
|
||||||
|
|
||||||
bullseye: *buster
|
bullseye: *buster
|
||||||
bookworm: *buster
|
bookworm: *buster
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
||||||
<Environment Id="ENV0"
|
<Environment Id="ENV0"
|
||||||
Name="PATH"
|
Name="PATH"
|
||||||
Value="[INSTALLDIR]."
|
Value="[INSTALLDIR]"
|
||||||
Permanent="no"
|
Permanent="no"
|
||||||
Part="last"
|
Part="last"
|
||||||
Action="create"
|
Action="create"
|
||||||
|
|
|
@ -3,6 +3,7 @@ package access
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"github.com/cloudflare/cloudflared/carrier"
|
"github.com/cloudflare/cloudflared/carrier"
|
||||||
"github.com/cloudflare/cloudflared/config"
|
"github.com/cloudflare/cloudflared/config"
|
||||||
"github.com/cloudflare/cloudflared/logger"
|
"github.com/cloudflare/cloudflared/logger"
|
||||||
|
"github.com/cloudflare/cloudflared/stream"
|
||||||
"github.com/cloudflare/cloudflared/validation"
|
"github.com/cloudflare/cloudflared/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,6 +40,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||||
if forwarder.TokenSecret != "" {
|
if forwarder.TokenSecret != "" {
|
||||||
headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret)
|
headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret)
|
||||||
}
|
}
|
||||||
|
headers.Set("User-Agent", userAgent)
|
||||||
|
|
||||||
carrier.SetBastionDest(headers, forwarder.Destination)
|
carrier.SetBastionDest(headers, forwarder.Destination)
|
||||||
|
|
||||||
|
@ -58,31 +61,37 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||||
// useful for proxying other protocols (like ssh) over websockets
|
// useful for proxying other protocols (like ssh) over websockets
|
||||||
// (which you can put Access in front of)
|
// (which you can put Access in front of)
|
||||||
func ssh(c *cli.Context) error {
|
func ssh(c *cli.Context) error {
|
||||||
log := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
|
// If not running as a forwarder, disable terminal logs as it collides with the stdin/stdout of the parent process
|
||||||
|
outputTerminal := logger.DisableTerminalLog
|
||||||
|
if c.IsSet(sshURLFlag) {
|
||||||
|
outputTerminal = logger.EnableTerminalLog
|
||||||
|
}
|
||||||
|
log := logger.CreateSSHLoggerFromContext(c, outputTerminal)
|
||||||
|
|
||||||
// get the hostname from the cmdline and error out if its not provided
|
// get the hostname from the cmdline and error out if its not provided
|
||||||
rawHostName := c.String(sshHostnameFlag)
|
rawHostName := c.String(sshHostnameFlag)
|
||||||
hostname, err := validation.ValidateHostname(rawHostName)
|
url, err := parseURL(rawHostName)
|
||||||
if err != nil || rawHostName == "" {
|
if err != nil {
|
||||||
|
log.Err(err).Send()
|
||||||
return cli.ShowCommandHelp(c, "ssh")
|
return cli.ShowCommandHelp(c, "ssh")
|
||||||
}
|
}
|
||||||
originURL := ensureURLScheme(hostname)
|
|
||||||
|
|
||||||
// get the headers from the cmdline and add them
|
// get the headers from the cmdline and add them
|
||||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||||
if c.IsSet(sshTokenIDFlag) {
|
if c.IsSet(sshTokenIDFlag) {
|
||||||
headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||||
}
|
}
|
||||||
if c.IsSet(sshTokenSecretFlag) {
|
if c.IsSet(sshTokenSecretFlag) {
|
||||||
headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
|
||||||
}
|
}
|
||||||
|
headers.Set("User-Agent", userAgent)
|
||||||
|
|
||||||
carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
|
carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
|
||||||
|
|
||||||
options := &carrier.StartOptions{
|
options := &carrier.StartOptions{
|
||||||
OriginURL: originURL,
|
OriginURL: url.String(),
|
||||||
Headers: headers,
|
Headers: headers,
|
||||||
Host: hostname,
|
Host: url.Host,
|
||||||
}
|
}
|
||||||
|
|
||||||
if connectTo := c.String(sshConnectTo); connectTo != "" {
|
if connectTo := c.String(sshConnectTo); connectTo != "" {
|
||||||
|
@ -121,16 +130,17 @@ func ssh(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
|
var s io.ReadWriter
|
||||||
}
|
s = &carrier.StdinoutStream{}
|
||||||
|
if c.IsSet(sshDebugStream) {
|
||||||
func buildRequestHeaders(values []string) http.Header {
|
maxMessages := c.Uint64(sshDebugStream)
|
||||||
headers := make(http.Header)
|
if maxMessages == 0 {
|
||||||
for _, valuePair := range values {
|
// default to 10 if provided but unset
|
||||||
split := strings.Split(valuePair, ":")
|
maxMessages = 10
|
||||||
if len(split) > 1 {
|
|
||||||
headers.Add(strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
|
|
||||||
}
|
}
|
||||||
|
logger := log.With().Str("host", url.Host).Logger()
|
||||||
|
s = stream.NewDebugStream(s, &logger, maxMessages)
|
||||||
}
|
}
|
||||||
return headers
|
carrier.StartClient(wsConn, s, options)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
package access
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuildRequestHeaders(t *testing.T) {
|
|
||||||
headers := make(http.Header)
|
|
||||||
headers.Add("client", "value")
|
|
||||||
headers.Add("secret", "safe-value")
|
|
||||||
|
|
||||||
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash"})
|
|
||||||
assert.Equal(t, headers.Get("client"), values.Get("client"))
|
|
||||||
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
|
|
||||||
}
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
loginQuietFlag = "quiet"
|
||||||
sshHostnameFlag = "hostname"
|
sshHostnameFlag = "hostname"
|
||||||
sshDestinationFlag = "destination"
|
sshDestinationFlag = "destination"
|
||||||
sshURLFlag = "url"
|
sshURLFlag = "url"
|
||||||
|
@ -34,6 +35,7 @@ const (
|
||||||
sshTokenSecretFlag = "service-token-secret"
|
sshTokenSecretFlag = "service-token-secret"
|
||||||
sshGenCertFlag = "short-lived-cert"
|
sshGenCertFlag = "short-lived-cert"
|
||||||
sshConnectTo = "connect-to"
|
sshConnectTo = "connect-to"
|
||||||
|
sshDebugStream = "debug-stream"
|
||||||
sshConfigTemplate = `
|
sshConfigTemplate = `
|
||||||
Add to your {{.Home}}/.ssh/config:
|
Add to your {{.Home}}/.ssh/config:
|
||||||
|
|
||||||
|
@ -89,6 +91,13 @@ func Commands() []*cli.Command {
|
||||||
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
|
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
|
||||||
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
|
||||||
administrator. cloudflared stores the token in local storage.`,
|
administrator. cloudflared stores the token in local storage.`,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: loginQuietFlag,
|
||||||
|
Aliases: []string{"q"},
|
||||||
|
Usage: "do not print the jwt to the command line",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "curl",
|
Name: "curl",
|
||||||
|
@ -123,15 +132,18 @@ func Commands() []*cli.Command {
|
||||||
Name: sshHostnameFlag,
|
Name: sshHostnameFlag,
|
||||||
Aliases: []string{"tunnel-host", "T"},
|
Aliases: []string{"tunnel-host", "T"},
|
||||||
Usage: "specify the hostname of your application.",
|
Usage: "specify the hostname of your application.",
|
||||||
|
EnvVars: []string{"TUNNEL_SERVICE_HOSTNAME"},
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: sshDestinationFlag,
|
Name: sshDestinationFlag,
|
||||||
Usage: "specify the destination address of your SSH server.",
|
Usage: "specify the destination address of your SSH server.",
|
||||||
|
EnvVars: []string{"TUNNEL_SERVICE_DESTINATION"},
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: sshURLFlag,
|
Name: sshURLFlag,
|
||||||
Aliases: []string{"listener", "L"},
|
Aliases: []string{"listener", "L"},
|
||||||
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
Usage: "specify the host:port to forward data to Cloudflare edge.",
|
||||||
|
EnvVars: []string{"TUNNEL_SERVICE_URL"},
|
||||||
},
|
},
|
||||||
&cli.StringSliceFlag{
|
&cli.StringSliceFlag{
|
||||||
Name: sshHeaderFlag,
|
Name: sshHeaderFlag,
|
||||||
|
@ -151,9 +163,12 @@ func Commands() []*cli.Command {
|
||||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
|
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: logger.LogSSHDirectoryFlag,
|
Name: logger.LogFileFlag,
|
||||||
Aliases: []string{"logfile"}, //added to match the tunnel side
|
Usage: "Save application log to this file for reporting issues.",
|
||||||
Usage: "Save application log to this directory for reporting issues.",
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: logger.LogSSHDirectoryFlag,
|
||||||
|
Usage: "Save application log to this directory for reporting issues.",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: logger.LogSSHLevelFlag,
|
Name: logger.LogSSHLevelFlag,
|
||||||
|
@ -165,6 +180,11 @@ func Commands() []*cli.Command {
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
|
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
|
||||||
},
|
},
|
||||||
|
&cli.Uint64Flag{
|
||||||
|
Name: sshDebugStream,
|
||||||
|
Hidden: true,
|
||||||
|
Usage: "Writes up-to the max provided stream payloads to the logger as debug statements.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -213,8 +233,7 @@ func login(c *cli.Context) error {
|
||||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||||
|
|
||||||
args := c.Args()
|
args := c.Args()
|
||||||
rawURL := ensureURLScheme(args.First())
|
appURL, err := parseURL(args.First())
|
||||||
appURL, err := url.Parse(rawURL)
|
|
||||||
if args.Len() < 1 || err != nil {
|
if args.Len() < 1 || err != nil {
|
||||||
log.Error().Msg("Please provide the url of the Access application")
|
log.Error().Msg("Please provide the url of the Access application")
|
||||||
return err
|
return err
|
||||||
|
@ -238,21 +257,15 @@ func login(c *cli.Context) error {
|
||||||
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
|
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
|
||||||
return errors.New("empty application token")
|
return errors.New("empty application token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Bool(loginQuietFlag) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
|
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureURLScheme prepends a URL with https:// if it doesn't have a scheme. http:// URLs will not be converted.
|
|
||||||
func ensureURLScheme(url string) string {
|
|
||||||
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
|
|
||||||
if !strings.HasPrefix(url, "https://") {
|
|
||||||
url = fmt.Sprintf("https://%s", url)
|
|
||||||
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
// curl provides a wrapper around curl, passing Access JWT along in request
|
// curl provides a wrapper around curl, passing Access JWT along in request
|
||||||
func curl(c *cli.Context) error {
|
func curl(c *cli.Context) error {
|
||||||
err := sentry.Init(sentry.ClientOptions{
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
|
@ -336,7 +349,7 @@ func generateToken(c *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
|
appURL, err := parseURL(c.String("app"))
|
||||||
if err != nil || c.NumFlags() < 1 {
|
if err != nil || c.NumFlags() < 1 {
|
||||||
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
fmt.Fprintln(os.Stderr, "Please provide a url.")
|
||||||
return err
|
return err
|
||||||
|
@ -389,7 +402,7 @@ func sshGen(c *cli.Context) error {
|
||||||
return cli.ShowCommandHelp(c, "ssh-gen")
|
return cli.ShowCommandHelp(c, "ssh-gen")
|
||||||
}
|
}
|
||||||
|
|
||||||
originURL, err := url.Parse(ensureURLScheme(hostname))
|
originURL, err := parseURL(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -468,6 +481,11 @@ func processURL(s string) (*url.URL, error) {
|
||||||
|
|
||||||
// cloudflaredPath pulls the full path of cloudflared on disk
|
// cloudflaredPath pulls the full path of cloudflared on disk
|
||||||
func cloudflaredPath() string {
|
func cloudflaredPath() string {
|
||||||
|
path, err := os.Executable()
|
||||||
|
if err == nil && isFileThere(path) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
for _, p := range strings.Split(os.Getenv("PATH"), ":") {
|
for _, p := range strings.Split(os.Getenv("PATH"), ":") {
|
||||||
path := fmt.Sprintf("%s/%s", p, "cloudflared")
|
path := fmt.Sprintf("%s/%s", p, "cloudflared")
|
||||||
if isFileThere(path) {
|
if isFileThere(path) {
|
||||||
|
@ -490,7 +508,7 @@ func isFileThere(candidate string) bool {
|
||||||
// Then makes a request to to the origin with the token to ensure it is valid.
|
// Then makes a request to to the origin with the token to ensure it is valid.
|
||||||
// Returns nil if token is valid.
|
// Returns nil if token is valid.
|
||||||
func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
|
func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
|
||||||
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
|
headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
|
||||||
if c.IsSet(sshTokenIDFlag) {
|
if c.IsSet(sshTokenIDFlag) {
|
||||||
headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
|
||||||
}
|
}
|
||||||
|
@ -525,6 +543,11 @@ func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, err
|
||||||
return false, errors.Wrap(err, "Could not create access request")
|
return false, errors.Wrap(err, "Could not create access request")
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", userAgent)
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
|
||||||
|
query := req.URL.Query()
|
||||||
|
query.Set("cloudflared_token_check", "true")
|
||||||
|
req.URL.RawQuery = query.Encode()
|
||||||
|
|
||||||
// Do not follow redirects
|
// Do not follow redirects
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
package access
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func Test_ensureURLScheme(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"no scheme", args{"localhost:123"}, "https://localhost:123"},
|
|
||||||
{"http scheme", args{"http://test"}, "https://test"},
|
|
||||||
{"https scheme", args{"https://test"}, "https://test"},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := ensureURLScheme(tt.args.url); got != tt.want {
|
|
||||||
t.Errorf("ensureURLScheme() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package access
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/http/httpguts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseRequestHeaders will take user-provided header values as strings "Content-Type: application/json" and create
|
||||||
|
// a http.Header object.
|
||||||
|
func parseRequestHeaders(values []string) http.Header {
|
||||||
|
headers := make(http.Header)
|
||||||
|
for _, valuePair := range values {
|
||||||
|
header, value, found := strings.Cut(valuePair, ":")
|
||||||
|
if found {
|
||||||
|
headers.Add(strings.TrimSpace(header), strings.TrimSpace(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHostname will attempt to convert a user provided URL string into a string with some light error checking on
|
||||||
|
// certain expectations from the URL.
|
||||||
|
// Will convert all HTTP URLs to HTTPS
|
||||||
|
func parseURL(input string) (*url.URL, error) {
|
||||||
|
if input == "" {
|
||||||
|
return nil, errors.New("no input provided")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(input, "https://") && !strings.HasPrefix(input, "http://") {
|
||||||
|
input = fmt.Sprintf("https://%s", input)
|
||||||
|
}
|
||||||
|
url, err := url.ParseRequestURI(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse as URL: %w", err)
|
||||||
|
}
|
||||||
|
if url.Scheme != "https" {
|
||||||
|
url.Scheme = "https"
|
||||||
|
}
|
||||||
|
if url.Host == "" {
|
||||||
|
return nil, errors.New("failed to parse Host")
|
||||||
|
}
|
||||||
|
host, err := httpguts.PunycodeHostPort(url.Host)
|
||||||
|
if err != nil || host == "" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !httpguts.ValidHostHeader(host) {
|
||||||
|
return nil, errors.New("invalid Host provided")
|
||||||
|
}
|
||||||
|
url.Host = host
|
||||||
|
return url, nil
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package access
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseRequestHeaders(t *testing.T) {
|
||||||
|
values := parseRequestHeaders([]string{"client: value", "secret: safe-value", "trash", "cf-trace-id: 000:000:0:1:asd"})
|
||||||
|
assert.Len(t, values, 3)
|
||||||
|
assert.Equal(t, "value", values.Get("client"))
|
||||||
|
assert.Equal(t, "safe-value", values.Get("secret"))
|
||||||
|
assert.Equal(t, "000:000:0:1:asd", values.Get("cf-trace-id"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseURL(t *testing.T) {
|
||||||
|
schemes := []string{
|
||||||
|
"http://",
|
||||||
|
"https://",
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
hosts := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"localhost", "localhost"},
|
||||||
|
{"127.0.0.1", "127.0.0.1"},
|
||||||
|
{"127.0.0.1:9090", "127.0.0.1:9090"},
|
||||||
|
{"::1", "::1"},
|
||||||
|
{"::1:8080", "::1:8080"},
|
||||||
|
{"[::1]", "[::1]"},
|
||||||
|
{"[::1]:8080", "[::1]:8080"},
|
||||||
|
{":8080", ":8080"},
|
||||||
|
{"example.com", "example.com"},
|
||||||
|
{"hello.example.com", "hello.example.com"},
|
||||||
|
{"bücher.example.com", "xn--bcher-kva.example.com"},
|
||||||
|
}
|
||||||
|
paths := []string{
|
||||||
|
"",
|
||||||
|
"/test",
|
||||||
|
"/example.com?qwe=123",
|
||||||
|
}
|
||||||
|
for i, scheme := range schemes {
|
||||||
|
for j, host := range hosts {
|
||||||
|
for k, path := range paths {
|
||||||
|
t.Run(fmt.Sprintf("%d_%d_%d", i, j, k), func(t *testing.T) {
|
||||||
|
input := fmt.Sprintf("%s%s%s", scheme, host.input, path)
|
||||||
|
expected := fmt.Sprintf("%s%s%s", "https://", host.expected, path)
|
||||||
|
url, err := parseURL(input)
|
||||||
|
assert.NoError(t, err, "input: %s\texpected: %s", input, expected)
|
||||||
|
assert.Equal(t, expected, url.String())
|
||||||
|
assert.Equal(t, host.expected, url.Host)
|
||||||
|
assert.Equal(t, "https", url.Scheme)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no input", func(t *testing.T) {
|
||||||
|
_, err := parseURL("")
|
||||||
|
assert.ErrorContains(t, err, "no input provided")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing host", func(t *testing.T) {
|
||||||
|
_, err := parseURL("https:///host")
|
||||||
|
assert.ErrorContains(t, err, "failed to parse Host")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid path only", func(t *testing.T) {
|
||||||
|
_, err := parseURL("/host")
|
||||||
|
assert.ErrorContains(t, err, "failed to parse Host")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid parse URL", func(t *testing.T) {
|
||||||
|
_, err := parseURL("https://host\\host")
|
||||||
|
assert.ErrorContains(t, err, "failed to parse as URL")
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !windows && !darwin && !linux
|
//go:build !windows && !darwin && !linux
|
||||||
// +build !windows,!darwin,!linux
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build linux
|
//go:build linux
|
||||||
// +build linux
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
@ -25,6 +24,9 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||||
Name: "install",
|
Name: "install",
|
||||||
Usage: "Install cloudflared as a system service",
|
Usage: "Install cloudflared as a system service",
|
||||||
Action: cliutil.ConfiguredAction(installLinuxService),
|
Action: cliutil.ConfiguredAction(installLinuxService),
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
noUpdateServiceFlag,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "uninstall",
|
Name: "uninstall",
|
||||||
|
@ -39,19 +41,22 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||||
// The directory and files that are used by the service.
|
// The directory and files that are used by the service.
|
||||||
// These are hard-coded in the templates below.
|
// These are hard-coded in the templates below.
|
||||||
const (
|
const (
|
||||||
serviceConfigDir = "/etc/cloudflared"
|
serviceConfigDir = "/etc/cloudflared"
|
||||||
serviceConfigFile = "config.yml"
|
serviceConfigFile = "config.yml"
|
||||||
serviceCredentialFile = "cert.pem"
|
serviceCredentialFile = "cert.pem"
|
||||||
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
|
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
|
||||||
cloudflaredService = "cloudflared.service"
|
cloudflaredService = "cloudflared.service"
|
||||||
|
cloudflaredUpdateService = "cloudflared-update.service"
|
||||||
|
cloudflaredUpdateTimer = "cloudflared-update.timer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var systemdTemplates = []ServiceTemplate{
|
var systemdAllTemplates = map[string]ServiceTemplate{
|
||||||
{
|
cloudflaredService: {
|
||||||
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
|
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
|
||||||
Content: `[Unit]
|
Content: `[Unit]
|
||||||
Description=cloudflared
|
Description=cloudflared
|
||||||
After=network.target
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
|
@ -64,18 +69,19 @@ RestartSec=5s
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
cloudflaredUpdateService: {
|
||||||
Path: "/etc/systemd/system/cloudflared-update.service",
|
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateService),
|
||||||
Content: `[Unit]
|
Content: `[Unit]
|
||||||
Description=Update cloudflared
|
Description=Update cloudflared
|
||||||
After=network.target
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
cloudflaredUpdateTimer: {
|
||||||
Path: "/etc/systemd/system/cloudflared-update.timer",
|
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateTimer),
|
||||||
Content: `[Unit]
|
Content: `[Unit]
|
||||||
Description=Update cloudflared
|
Description=Update cloudflared
|
||||||
|
|
||||||
|
@ -106,7 +112,7 @@ var sysvTemplate = ServiceTemplate{
|
||||||
# Description: cloudflared agent
|
# Description: cloudflared agent
|
||||||
### END INIT INFO
|
### END INIT INFO
|
||||||
name=$(basename $(readlink -f $0))
|
name=$(basename $(readlink -f $0))
|
||||||
cmd="{{.Path}} --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
cmd="{{.Path}} --pidfile /var/run/$name.pid {{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||||
pid_file="/var/run/$name.pid"
|
pid_file="/var/run/$name.pid"
|
||||||
stdout_log="/var/log/$name.log"
|
stdout_log="/var/log/$name.log"
|
||||||
stderr_log="/var/log/$name.err"
|
stderr_log="/var/log/$name.err"
|
||||||
|
@ -178,6 +184,14 @@ exit 0
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
noUpdateServiceFlag = &cli.BoolFlag{
|
||||||
|
Name: "no-update-service",
|
||||||
|
Usage: "Disable auto-update of the cloudflared linux service, which restarts the server to upgrade for new versions.",
|
||||||
|
Value: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func isSystemd() bool {
|
func isSystemd() bool {
|
||||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
||||||
return true
|
return true
|
||||||
|
@ -196,6 +210,9 @@ func installLinuxService(c *cli.Context) error {
|
||||||
Path: etPath,
|
Path: etPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the "no update flag" is set
|
||||||
|
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
|
||||||
|
|
||||||
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
|
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
|
||||||
if c.NArg() == 0 {
|
if c.NArg() == 0 {
|
||||||
extraArgsFunc = buildArgsForConfig
|
extraArgsFunc = buildArgsForConfig
|
||||||
|
@ -213,10 +230,10 @@ func installLinuxService(c *cli.Context) error {
|
||||||
switch {
|
switch {
|
||||||
case isSystemd():
|
case isSystemd():
|
||||||
log.Info().Msgf("Using Systemd")
|
log.Info().Msgf("Using Systemd")
|
||||||
err = installSystemd(&templateArgs, log)
|
err = installSystemd(&templateArgs, autoUpdate, log)
|
||||||
default:
|
default:
|
||||||
log.Info().Msgf("Using SysV")
|
log.Info().Msgf("Using SysV")
|
||||||
err = installSysv(&templateArgs, log)
|
err = installSysv(&templateArgs, autoUpdate, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -261,7 +278,20 @@ credentials-file: CREDENTIALS-FILE
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
func installSystemd(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error {
|
||||||
|
var systemdTemplates []ServiceTemplate
|
||||||
|
if autoUpdate {
|
||||||
|
systemdTemplates = []ServiceTemplate{
|
||||||
|
systemdAllTemplates[cloudflaredService],
|
||||||
|
systemdAllTemplates[cloudflaredUpdateService],
|
||||||
|
systemdAllTemplates[cloudflaredUpdateTimer],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
systemdTemplates = []ServiceTemplate{
|
||||||
|
systemdAllTemplates[cloudflaredService],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, serviceTemplate := range systemdTemplates {
|
for _, serviceTemplate := range systemdTemplates {
|
||||||
err := serviceTemplate.Generate(templateArgs)
|
err := serviceTemplate.Generate(templateArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -273,10 +303,14 @@ func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) erro
|
||||||
log.Err(err).Msgf("systemctl enable %s error", cloudflaredService)
|
log.Err(err).Msgf("systemctl enable %s error", cloudflaredService)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
|
|
||||||
log.Err(err).Msg("systemctl start cloudflared-update.timer error")
|
if autoUpdate {
|
||||||
return err
|
if err := runCommand("systemctl", "start", cloudflaredUpdateTimer); err != nil {
|
||||||
|
log.Err(err).Msgf("systemctl start %s error", cloudflaredUpdateTimer)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runCommand("systemctl", "daemon-reload"); err != nil {
|
if err := runCommand("systemctl", "daemon-reload"); err != nil {
|
||||||
log.Err(err).Msg("systemctl daemon-reload error")
|
log.Err(err).Msg("systemctl daemon-reload error")
|
||||||
return err
|
return err
|
||||||
|
@ -284,12 +318,19 @@ func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) erro
|
||||||
return runCommand("systemctl", "start", cloudflaredService)
|
return runCommand("systemctl", "start", cloudflaredService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
func installSysv(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error {
|
||||||
confPath, err := sysvTemplate.ResolvePath()
|
confPath, err := sysvTemplate.ResolvePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("error resolving system path")
|
log.Err(err).Msg("error resolving system path")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if autoUpdate {
|
||||||
|
templateArgs.ExtraArgs = append([]string{"--autoupdate-freq 24h0m0s"}, templateArgs.ExtraArgs...)
|
||||||
|
} else {
|
||||||
|
templateArgs.ExtraArgs = append([]string{"--no-autoupdate"}, templateArgs.ExtraArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
if err := sysvTemplate.Generate(templateArgs); err != nil {
|
||||||
log.Err(err).Msg("error generating system template")
|
log.Err(err).Msg("error generating system template")
|
||||||
return err
|
return err
|
||||||
|
@ -327,19 +368,35 @@ func uninstallLinuxService(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func uninstallSystemd(log *zerolog.Logger) error {
|
func uninstallSystemd(log *zerolog.Logger) error {
|
||||||
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
|
// Get only the installed services
|
||||||
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
|
installedServices := make(map[string]ServiceTemplate)
|
||||||
return err
|
for serviceName, serviceTemplate := range systemdAllTemplates {
|
||||||
|
if err := runCommand("systemctl", "list-units", "--all", "|", "grep", serviceName); err == nil {
|
||||||
|
installedServices[serviceName] = serviceTemplate
|
||||||
|
} else {
|
||||||
|
log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
|
|
||||||
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
|
if _, exists := installedServices[cloudflaredService]; exists {
|
||||||
return err
|
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
|
||||||
|
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
|
||||||
|
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
|
|
||||||
log.Err(err).Msg("systemctl stop cloudflared-update.timer error")
|
if _, exists := installedServices[cloudflaredUpdateTimer]; exists {
|
||||||
return err
|
if err := runCommand("systemctl", "stop", cloudflaredUpdateTimer); err != nil {
|
||||||
|
log.Err(err).Msgf("systemctl stop %s error", cloudflaredUpdateTimer)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, serviceTemplate := range systemdTemplates {
|
|
||||||
|
for _, serviceTemplate := range installedServices {
|
||||||
if err := serviceTemplate.Remove(); err != nil {
|
if err := serviceTemplate.Remove(); err != nil {
|
||||||
log.Err(err).Msg("error removing service template")
|
log.Err(err).Msg("error removing service template")
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build darwin
|
//go:build darwin
|
||||||
// +build darwin
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -49,6 +50,9 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// FIXME: TUN-8148: Disable QUIC_GO ECN due to bugs in proper detection if supported
|
||||||
|
os.Setenv("QUIC_GO_DISABLE_ECN", "1")
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||||
maxprocs.Set()
|
maxprocs.Set()
|
||||||
|
@ -130,11 +134,22 @@ To determine if an update happened in a script, check for error code 11.`,
|
||||||
{
|
{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Action: func(c *cli.Context) (err error) {
|
Action: func(c *cli.Context) (err error) {
|
||||||
|
if c.Bool("short") {
|
||||||
|
fmt.Println(strings.Split(c.App.Version, " ")[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
version(c)
|
version(c)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Usage: versionText,
|
Usage: versionText,
|
||||||
Description: versionText,
|
Description: versionText,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "short",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "print just the version number",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmds = append(cmds, tunnel.Commands()...)
|
cmds = append(cmds, tunnel.Commands()...)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
@ -64,7 +63,7 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
|
||||||
return fmt.Errorf("error creating %s: %v", plistFolder, err)
|
return fmt.Errorf("error creating %s: %v", plistFolder, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(resolvedPath, buffer.Bytes(), fileMode)
|
err = os.WriteFile(resolvedPath, buffer.Bytes(), fileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error writing %s: %v", resolvedPath, err)
|
return fmt.Errorf("error writing %s: %v", resolvedPath, err)
|
||||||
}
|
}
|
||||||
|
@ -103,7 +102,7 @@ func runCommand(command string, args ...string) error {
|
||||||
return fmt.Errorf("error starting %s: %v", command, err)
|
return fmt.Errorf("error starting %s: %v", command, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, _ := ioutil.ReadAll(stderr)
|
output, _ := io.ReadAll(stderr)
|
||||||
err = cmd.Wait()
|
err = cmd.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s %v returned with error code %v due to: %v", command, args, err, string(output))
|
return fmt.Errorf("%s %v returned with error code %v due to: %v", command, args, err, string(output))
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"runtime/trace"
|
"runtime/trace"
|
||||||
|
@ -12,7 +11,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/daemon"
|
"github.com/coreos/go-systemd/v22/daemon"
|
||||||
"github.com/facebookgo/grace/gracenet"
|
"github.com/facebookgo/grace/gracenet"
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -79,6 +78,17 @@ const (
|
||||||
// hostKeyPath is the path of the dir to save SSH host keys too
|
// hostKeyPath is the path of the dir to save SSH host keys too
|
||||||
hostKeyPath = "host-key-path"
|
hostKeyPath = "host-key-path"
|
||||||
|
|
||||||
|
// udpUnregisterSessionTimeout is how long we wait before we stop trying to unregister a UDP session from the edge
|
||||||
|
udpUnregisterSessionTimeoutFlag = "udp-unregister-session-timeout"
|
||||||
|
|
||||||
|
// writeStreamTimeout sets if we should have a timeout when writing data to a stream towards the destination (edge/origin).
|
||||||
|
writeStreamTimeout = "write-stream-timeout"
|
||||||
|
|
||||||
|
// quicDisablePathMTUDiscovery sets if QUIC should not perform PTMU discovery and use a smaller (safe) packet size.
|
||||||
|
// Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
|
||||||
|
// Note that this may result in packet drops for UDP proxying, since we expect being able to send at least 1280 bytes of inner packets.
|
||||||
|
quicDisablePathMTUDiscovery = "quic-disable-pmtu-discovery"
|
||||||
|
|
||||||
// uiFlag is to enable launching cloudflared in interactive UI mode
|
// uiFlag is to enable launching cloudflared in interactive UI mode
|
||||||
uiFlag = "ui"
|
uiFlag = "ui"
|
||||||
|
|
||||||
|
@ -297,7 +307,7 @@ func StartServer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.IsSet("trace-output") {
|
if c.IsSet("trace-output") {
|
||||||
tmpTraceFile, err := ioutil.TempFile("", "trace")
|
tmpTraceFile, err := os.CreateTemp("", "trace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to create new temporary file to save trace output")
|
log.Err(err).Msg("Failed to create new temporary file to save trace output")
|
||||||
}
|
}
|
||||||
|
@ -333,7 +343,7 @@ func StartServer(
|
||||||
logClientOptions(c, log)
|
logClientOptions(c, log)
|
||||||
|
|
||||||
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
|
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(c.Context)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
go waitForSignal(graceShutdownC, log)
|
go waitForSignal(graceShutdownC, log)
|
||||||
|
@ -385,7 +395,7 @@ func StartServer(
|
||||||
observer.SendURL(quickTunnelURL)
|
observer.SendURL(quickTunnelURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnelConfig, orchestratorConfig, err := prepareTunnelConfig(c, info, log, logTransport, observer, namedTunnel)
|
tunnelConfig, orchestratorConfig, err := prepareTunnelConfig(ctx, c, info, log, logTransport, observer, namedTunnel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Couldn't start tunnel")
|
log.Err(err).Msg("Couldn't start tunnel")
|
||||||
return err
|
return err
|
||||||
|
@ -399,7 +409,7 @@ func StartServer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
localRules := []ingress.Rule{}
|
internalRules := []ingress.Rule{}
|
||||||
if features.Contains(features.FeatureManagementLogs) {
|
if features.Contains(features.FeatureManagementLogs) {
|
||||||
serviceIP := c.String("service-op-ip")
|
serviceIP := c.String("service-op-ip")
|
||||||
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
|
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
|
||||||
|
@ -410,15 +420,16 @@ func StartServer(
|
||||||
|
|
||||||
mgmt := management.New(
|
mgmt := management.New(
|
||||||
c.String("management-hostname"),
|
c.String("management-hostname"),
|
||||||
|
c.Bool("management-diagnostics"),
|
||||||
serviceIP,
|
serviceIP,
|
||||||
clientID,
|
clientID,
|
||||||
c.String(connectorLabelFlag),
|
c.String(connectorLabelFlag),
|
||||||
logger.ManagementLogger.Log,
|
logger.ManagementLogger.Log,
|
||||||
logger.ManagementLogger,
|
logger.ManagementLogger,
|
||||||
)
|
)
|
||||||
localRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
|
internalRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
|
||||||
}
|
}
|
||||||
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, localRules, tunnelConfig.Log)
|
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -683,6 +694,25 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||||
Value: 4,
|
Value: 4,
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
}),
|
}),
|
||||||
|
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||||
|
Name: udpUnregisterSessionTimeoutFlag,
|
||||||
|
Value: 5 * time.Second,
|
||||||
|
Hidden: true,
|
||||||
|
}),
|
||||||
|
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||||
|
Name: writeStreamTimeout,
|
||||||
|
EnvVars: []string{"TUNNEL_STREAM_WRITE_TIMEOUT"},
|
||||||
|
Usage: "Use this option to add a stream write timeout for connections when writing towards the origin or edge. Default is 0 which disables the write timeout.",
|
||||||
|
Value: 0 * time.Second,
|
||||||
|
Hidden: true,
|
||||||
|
}),
|
||||||
|
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||||
|
Name: quicDisablePathMTUDiscovery,
|
||||||
|
EnvVars: []string{"TUNNEL_DISABLE_QUIC_PMTU"},
|
||||||
|
Usage: "Use this option to disable PTMU discovery for QUIC connections. This will result in lower packet sizes. Not however, that this may cause instability for UDP proxying.",
|
||||||
|
Value: false,
|
||||||
|
Hidden: true,
|
||||||
|
}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
altsrc.NewStringFlag(&cli.StringFlag{
|
||||||
Name: connectorLabelFlag,
|
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.",
|
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.",
|
||||||
|
@ -756,6 +786,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||||
EnvVars: []string{"TUNNEL_POST_QUANTUM"},
|
EnvVars: []string{"TUNNEL_POST_QUANTUM"},
|
||||||
Hidden: FipsEnabled,
|
Hidden: FipsEnabled,
|
||||||
}),
|
}),
|
||||||
|
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||||
|
Name: "management-diagnostics",
|
||||||
|
Usage: "Enables the in-depth diagnostic routes to be made available over the management service (/debug/pprof, /metrics, etc.)",
|
||||||
|
EnvVars: []string{"TUNNEL_MANAGEMENT_DIAGNOSTICS"},
|
||||||
|
Value: true,
|
||||||
|
}),
|
||||||
selectProtocolFlag,
|
selectProtocolFlag,
|
||||||
overwriteDNSFlag,
|
overwriteDNSFlag,
|
||||||
}...)
|
}...)
|
||||||
|
|
|
@ -4,10 +4,12 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflared/features"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDedup(t *testing.T) {
|
func TestDedup(t *testing.T) {
|
||||||
expected := []string{"a", "b"}
|
expected := []string{"a", "b"}
|
||||||
actual := dedup([]string{"a", "b", "a"})
|
actual := features.Dedup([]string{"a", "b", "a"})
|
||||||
require.ElementsMatch(t, expected, actual)
|
require.ElementsMatch(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
mathRand "math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
@ -15,7 +15,7 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
"github.com/cloudflare/cloudflared/config"
|
"github.com/cloudflare/cloudflared/config"
|
||||||
|
@ -30,12 +30,15 @@ import (
|
||||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const secretValue = "*****"
|
const (
|
||||||
|
secretValue = "*****"
|
||||||
|
icmpFunnelTimeout = time.Second * 10
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
|
developerPortal = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup"
|
||||||
serviceUrl = developerPortal + "/reference/service/"
|
serviceUrl = developerPortal + "/tunnel-guide/local/as-a-service/"
|
||||||
argumentsUrl = developerPortal + "/reference/arguments/"
|
argumentsUrl = developerPortal + "/tunnel-guide/local/local-management/arguments/"
|
||||||
|
|
||||||
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
|
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
|
||||||
|
|
||||||
|
@ -113,6 +116,7 @@ func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelPrope
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareTunnelConfig(
|
func prepareTunnelConfig(
|
||||||
|
ctx context.Context,
|
||||||
c *cli.Context,
|
c *cli.Context,
|
||||||
info *cliutil.BuildInfo,
|
info *cliutil.BuildInfo,
|
||||||
log, logTransport *zerolog.Logger,
|
log, logTransport *zerolog.Logger,
|
||||||
|
@ -132,22 +136,36 @@ func prepareTunnelConfig(
|
||||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID.String()})
|
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID.String()})
|
||||||
|
|
||||||
transportProtocol := c.String("protocol")
|
transportProtocol := c.String("protocol")
|
||||||
needPQ := c.Bool("post-quantum")
|
|
||||||
if needPQ {
|
clientFeatures := features.Dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
|
||||||
|
|
||||||
|
staticFeatures := features.StaticFeatures{}
|
||||||
|
if c.Bool("post-quantum") {
|
||||||
if FipsEnabled {
|
if FipsEnabled {
|
||||||
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode")
|
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode")
|
||||||
}
|
}
|
||||||
|
pqMode := features.PostQuantumStrict
|
||||||
|
staticFeatures.PostQuantumMode = &pqMode
|
||||||
|
}
|
||||||
|
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, staticFeatures, log)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
|
||||||
|
}
|
||||||
|
pqMode := featureSelector.PostQuantumMode()
|
||||||
|
if pqMode == features.PostQuantumStrict {
|
||||||
// Error if the user tries to force a non-quic transport protocol
|
// Error if the user tries to force a non-quic transport protocol
|
||||||
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
|
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
|
||||||
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
|
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
|
||||||
}
|
}
|
||||||
transportProtocol = connection.QUIC.String()
|
transportProtocol = connection.QUIC.String()
|
||||||
|
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
|
||||||
|
|
||||||
|
log.Info().Msgf(
|
||||||
|
"Using hybrid post-quantum key agreement %s",
|
||||||
|
supervisor.PQKexName,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientFeatures := dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
|
|
||||||
if needPQ {
|
|
||||||
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
|
|
||||||
}
|
|
||||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||||
ClientID: clientID[:],
|
ClientID: clientID[:],
|
||||||
Features: clientFeatures,
|
Features: clientFeatures,
|
||||||
|
@ -203,15 +221,6 @@ func prepareTunnelConfig(
|
||||||
log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version")
|
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{
|
tunnelConfig := &supervisor.TunnelConfig{
|
||||||
GracePeriod: gracePeriod,
|
GracePeriod: gracePeriod,
|
||||||
ReplaceExisting: c.Bool("force"),
|
ReplaceExisting: c.Bool("force"),
|
||||||
|
@ -222,7 +231,6 @@ func prepareTunnelConfig(
|
||||||
EdgeIPVersion: edgeIPVersion,
|
EdgeIPVersion: edgeIPVersion,
|
||||||
EdgeBindAddr: edgeBindAddr,
|
EdgeBindAddr: edgeBindAddr,
|
||||||
HAConnections: c.Int(haConnectionsFlag),
|
HAConnections: c.Int(haConnectionsFlag),
|
||||||
IncidentLookup: supervisor.NewIncidentLookup(),
|
|
||||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||||
LBPool: c.String("lb-pool"),
|
LBPool: c.String("lb-pool"),
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
@ -231,14 +239,16 @@ func prepareTunnelConfig(
|
||||||
Observer: observer,
|
Observer: observer,
|
||||||
ReportedVersion: info.Version(),
|
ReportedVersion: info.Version(),
|
||||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||||
Retries: uint(c.Int("retries")),
|
Retries: uint(c.Int("retries")),
|
||||||
RunFromTerminal: isRunningFromTerminal(),
|
RunFromTerminal: isRunningFromTerminal(),
|
||||||
NamedTunnel: namedTunnel,
|
NamedTunnel: namedTunnel,
|
||||||
ProtocolSelector: protocolSelector,
|
ProtocolSelector: protocolSelector,
|
||||||
EdgeTLSConfigs: edgeTLSConfigs,
|
EdgeTLSConfigs: edgeTLSConfigs,
|
||||||
NeedPQ: needPQ,
|
FeatureSelector: featureSelector,
|
||||||
PQKexIdx: pqKexIdx,
|
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
|
||||||
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
|
UDPUnregisterSessionTimeout: c.Duration(udpUnregisterSessionTimeoutFlag),
|
||||||
|
WriteStreamTimeout: c.Duration(writeStreamTimeout),
|
||||||
|
DisableQUICPathMTUDiscovery: c.Bool(quicDisablePathMTUDiscovery),
|
||||||
}
|
}
|
||||||
packetConfig, err := newPacketConfig(c, log)
|
packetConfig, err := newPacketConfig(c, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -250,6 +260,7 @@ func prepareTunnelConfig(
|
||||||
Ingress: &ingressRules,
|
Ingress: &ingressRules,
|
||||||
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
|
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
|
||||||
ConfigurationFlags: parseConfigFlags(c),
|
ConfigurationFlags: parseConfigFlags(c),
|
||||||
|
WriteTimeout: c.Duration(writeStreamTimeout),
|
||||||
}
|
}
|
||||||
return tunnelConfig, orchestratorConfig, nil
|
return tunnelConfig, orchestratorConfig, nil
|
||||||
}
|
}
|
||||||
|
@ -275,26 +286,7 @@ func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isRunningFromTerminal() bool {
|
func isRunningFromTerminal() bool {
|
||||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any duplicates from the slice
|
|
||||||
func dedup(slice []string) []string {
|
|
||||||
|
|
||||||
// Convert the slice into a set
|
|
||||||
set := make(map[string]bool, 0)
|
|
||||||
for _, str := range slice {
|
|
||||||
set[str] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the set back into a slice
|
|
||||||
keys := make([]string, len(set))
|
|
||||||
i := 0
|
|
||||||
for str := range set {
|
|
||||||
keys[i] = str
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseConfigIPVersion returns the IP version from possible expected values from config
|
// ParseConfigIPVersion returns the IP version from possible expected values from config
|
||||||
|
@ -374,7 +366,7 @@ func newPacketConfig(c *cli.Context, logger *zerolog.Logger) (*ingress.GlobalRou
|
||||||
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
|
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
|
||||||
}
|
}
|
||||||
|
|
||||||
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, zone, logger)
|
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, zone, logger, icmpFunnelTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build ignore
|
//go:build ignore
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,5 +22,5 @@ func (fs realFileSystem) validFilePath(path string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
|
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
|
||||||
return ioutil.ReadFile(filePath)
|
return os.ReadFile(filePath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,7 +139,7 @@ func testURLCommand(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
|
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
|
||||||
fmt.Printf("Matched rule #%d\n", i+1)
|
fmt.Printf("Matched rule #%d\n", i)
|
||||||
fmt.Println(ing.Rules[i].MultiLineString())
|
fmt.Println(ing.Rules[i].MultiLineString())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -66,7 +65,7 @@ func login(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
|
if err := os.WriteFile(path, resourceData, 0600); err != nil {
|
||||||
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||||
var errorLines []string
|
var errorLines []string
|
||||||
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath))
|
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath))
|
||||||
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
|
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
|
||||||
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
|
if deleteErr := client.DeleteTunnel(tunnel.ID, true); deleteErr != nil {
|
||||||
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
|
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
|
||||||
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
|
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
|
||||||
} else {
|
} else {
|
||||||
|
@ -206,13 +206,8 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||||
if !tunnel.DeletedAt.IsZero() {
|
if !tunnel.DeletedAt.IsZero() {
|
||||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||||
}
|
}
|
||||||
if forceFlagSet {
|
|
||||||
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
|
|
||||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.DeleteTunnel(tunnel.ID); err != nil {
|
if err := client.DeleteTunnel(tunnel.ID, forceFlagSet); err != nil {
|
||||||
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
|
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/cfapi"
|
"github.com/cloudflare/cloudflared/cfapi"
|
||||||
|
@ -24,12 +27,12 @@ func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, err
|
||||||
return client.AddRoute(newRoute)
|
return client.AddRoute(newRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
|
func (sc *subcommandContext) deleteRoute(id uuid.UUID) error {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, noClientMsg)
|
return errors.Wrap(err, noClientMsg)
|
||||||
}
|
}
|
||||||
return client.DeleteRoute(params)
|
return client.DeleteRoute(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
||||||
|
@ -39,3 +42,25 @@ func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfap
|
||||||
}
|
}
|
||||||
return client.GetByIP(params)
|
return client.GetByIP(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *subcommandContext) getRouteId(network net.IPNet, vnetId *uuid.UUID) (uuid.UUID, error) {
|
||||||
|
filters := cfapi.NewIPRouteFilter()
|
||||||
|
filters.NotDeleted()
|
||||||
|
filters.NetworkIsSubsetOf(network)
|
||||||
|
filters.NetworkIsSupersetOf(network)
|
||||||
|
|
||||||
|
if vnetId != nil {
|
||||||
|
filters.VNetID(*vnetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := sc.listRoutes(filters)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) != 1 {
|
||||||
|
return uuid.Nil, errors.New("unable to find route for provided network and vnet")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[0].ID, nil
|
||||||
|
}
|
||||||
|
|
|
@ -219,7 +219,7 @@ func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, erro
|
||||||
return "token", nil
|
return "token", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
|
||||||
tunnel, ok := d.mockTunnels[tunnelID]
|
tunnel, ok := d.mockTunnels[tunnelID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -120,8 +119,8 @@ var (
|
||||||
forceDeleteFlag = &cli.BoolFlag{
|
forceDeleteFlag = &cli.BoolFlag{
|
||||||
Name: "force",
|
Name: "force",
|
||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Usage: "Cleans up any stale connections before the tunnel is deleted. cloudflared will not " +
|
Usage: "Deletes a tunnel even if tunnel is connected and it has dependencies associated to it. (eg. IP routes)." +
|
||||||
"delete a tunnel with connections without this flag.",
|
" It is not possible to delete tunnels that have connections or non-deleted dependencies, without this flag.",
|
||||||
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
|
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
|
||||||
}
|
}
|
||||||
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||||
|
@ -241,7 +240,7 @@ func writeTunnelCredentials(filePath string, credentials *connection.Credentials
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
|
||||||
}
|
}
|
||||||
return ioutil.WriteFile(filePath, body, 400)
|
return os.WriteFile(filePath, body, 0400)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildListCommand() *cli.Command {
|
func buildListCommand() *cli.Command {
|
||||||
|
|
|
@ -21,6 +21,8 @@ var (
|
||||||
Aliases: []string{"vn"},
|
Aliases: []string{"vn"},
|
||||||
Usage: "The ID or name of the virtual network to which the route is associated to.",
|
Usage: "The ID or name of the virtual network to which the route is associated to.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routeAddError = errors.New("You must supply exactly one argument, the ID or CIDR of the route you want to delete")
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildRouteIPSubcommand() *cli.Command {
|
func buildRouteIPSubcommand() *cli.Command {
|
||||||
|
@ -68,11 +70,9 @@ which virtual network's routing table you want to add the route to with:
|
||||||
Name: "delete",
|
Name: "delete",
|
||||||
Action: cliutil.ConfiguredAction(deleteRouteCommand),
|
Action: cliutil.ConfiguredAction(deleteRouteCommand),
|
||||||
Usage: "Delete a row from your organization's private routing table",
|
Usage: "Delete a row from your organization's private routing table",
|
||||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]",
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [Route ID or CIDR]",
|
||||||
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network
|
Description: `Deletes the row for the given route ID from your routing table. That portion of your network
|
||||||
will no longer be reachable by the WARP clients. Note that if you use virtual
|
will no longer be reachable.`,
|
||||||
networks, then you have to tell which virtual network whose routing table you
|
|
||||||
have a row deleted from.`,
|
|
||||||
Flags: []cli.Flag{vnetFlag},
|
Flags: []cli.Flag{vnetFlag},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -187,33 +187,36 @@ func deleteRouteCommand(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.NArg() != 1 {
|
if c.NArg() != 1 {
|
||||||
return errors.New("You must supply exactly one argument, the network whose route you want to delete (in CIDR form e.g. 1.2.3.4/32)")
|
return routeAddError
|
||||||
}
|
}
|
||||||
|
|
||||||
_, network, err := net.ParseCIDR(c.Args().First())
|
var routeId uuid.UUID
|
||||||
|
routeId, err = uuid.Parse(c.Args().First())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Invalid network CIDR")
|
_, network, err := net.ParseCIDR(c.Args().First())
|
||||||
}
|
if err != nil || network == nil {
|
||||||
if network == nil {
|
return routeAddError
|
||||||
return errors.New("Invalid network CIDR")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
params := cfapi.DeleteRouteParams{
|
var vnetId *uuid.UUID
|
||||||
Network: *network,
|
if c.IsSet(vnetFlag.Name) {
|
||||||
}
|
id, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vnetId = &id
|
||||||
|
}
|
||||||
|
|
||||||
if c.IsSet(vnetFlag.Name) {
|
routeId, err = sc.getRouteId(*network, vnetId)
|
||||||
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
params.VNetID = &vnetId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sc.deleteRoute(params); err != nil {
|
if err := sc.deleteRoute(routeId); err != nil {
|
||||||
return errors.Wrap(err, "API error")
|
return errors.Wrap(err, "API error")
|
||||||
}
|
}
|
||||||
fmt.Printf("Successfully deleted route for %s\n", network)
|
fmt.Printf("Successfully deleted route with ID %s\n", routeId)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +272,7 @@ func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
|
||||||
defer writer.Flush()
|
defer writer.Flush()
|
||||||
|
|
||||||
// Print column headers with tabbed columns
|
// Print column headers with tabbed columns
|
||||||
_, _ = fmt.Fprintln(writer, "NETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
_, _ = fmt.Fprintln(writer, "ID\tNETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
||||||
|
|
||||||
// Loop through routes, create formatted string for each, and print using tabwriter
|
// Loop through routes, create formatted string for each, and print using tabwriter
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/facebookgo/grace/gracenet"
|
"github.com/facebookgo/grace/gracenet"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/config"
|
"github.com/cloudflare/cloudflared/config"
|
||||||
"github.com/cloudflare/cloudflared/logger"
|
"github.com/cloudflare/cloudflared/logger"
|
||||||
|
@ -304,7 +304,7 @@ func wasInstalledFromPackageManager() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isRunningFromTerminal() bool {
|
func isRunningFromTerminal() bool {
|
||||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsSysV() bool {
|
func IsSysV() bool {
|
||||||
|
|
|
@ -56,6 +56,9 @@ func (s *WorkersService) Check() (CheckResult, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, s.url, nil)
|
req, err := http.NewRequest(http.MethodGet, s.url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
q.Add(OSKeyName, runtime.GOOS)
|
q.Add(OSKeyName, runtime.GOOS)
|
||||||
q.Add(ArchitectureKeyName, runtime.GOARCH)
|
q.Add(ArchitectureKeyName, runtime.GOARCH)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package updater
|
package updater
|
||||||
|
|
||||||
|
@ -11,7 +10,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -224,7 +222,7 @@ func TestUpdateService(t *testing.T) {
|
||||||
require.Equal(t, v.Version(), mostRecentVersion)
|
require.Equal(t, v.Version(), mostRecentVersion)
|
||||||
|
|
||||||
require.NoError(t, v.Apply())
|
require.NoError(t, v.Apply())
|
||||||
dat, err := ioutil.ReadFile(testFilePath)
|
dat, err := os.ReadFile(testFilePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, string(dat), mostRecentVersion)
|
require.Equal(t, string(dat), mostRecentVersion)
|
||||||
|
@ -243,7 +241,7 @@ func TestBetaUpdateService(t *testing.T) {
|
||||||
require.Equal(t, v.Version(), mostRecentBetaVersion)
|
require.Equal(t, v.Version(), mostRecentBetaVersion)
|
||||||
|
|
||||||
require.NoError(t, v.Apply())
|
require.NoError(t, v.Apply())
|
||||||
dat, err := ioutil.ReadFile(testFilePath)
|
dat, err := os.ReadFile(testFilePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, string(dat), mostRecentBetaVersion)
|
require.Equal(t, string(dat), mostRecentBetaVersion)
|
||||||
|
@ -289,7 +287,7 @@ func TestForcedUpdateService(t *testing.T) {
|
||||||
require.Equal(t, v.Version(), mostRecentVersion)
|
require.Equal(t, v.Version(), mostRecentVersion)
|
||||||
|
|
||||||
require.NoError(t, v.Apply())
|
require.NoError(t, v.Apply())
|
||||||
dat, err := ioutil.ReadFile(testFilePath)
|
dat, err := os.ReadFile(testFilePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, string(dat), mostRecentVersion)
|
require.Equal(t, string(dat), mostRecentVersion)
|
||||||
|
@ -309,7 +307,7 @@ func TestUpdateSpecificVersionService(t *testing.T) {
|
||||||
require.Equal(t, reqVersion, v.Version())
|
require.Equal(t, reqVersion, v.Version())
|
||||||
|
|
||||||
require.NoError(t, v.Apply())
|
require.NoError(t, v.Apply())
|
||||||
dat, err := ioutil.ReadFile(testFilePath)
|
dat, err := os.ReadFile(testFilePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, reqVersion, string(dat))
|
require.Equal(t, reqVersion, string(dat))
|
||||||
|
@ -328,7 +326,7 @@ func TestCompressedUpdateService(t *testing.T) {
|
||||||
require.Equal(t, "2020.09.02", v.Version())
|
require.Equal(t, "2020.09.02", v.Version())
|
||||||
|
|
||||||
require.NoError(t, v.Apply())
|
require.NoError(t, v.Apply())
|
||||||
dat, err := ioutil.ReadFile(testFilePath)
|
dat, err := os.ReadFile(testFilePath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, "2020.09.02", string(dat))
|
require.Equal(t, "2020.09.02", string(dat))
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
cloudflare==2.8.15
|
cloudflare==2.14.3
|
||||||
flaky==3.7.0
|
flaky==3.7.0
|
||||||
pytest==7.3.1
|
pytest==7.3.1
|
||||||
pytest-asyncio==0.21.0
|
pytest-asyncio==0.21.0
|
||||||
pyyaml==5.4.1
|
pyyaml==6.0.1
|
||||||
requests==2.28.2
|
requests==2.28.2
|
||||||
retrying==1.3.4
|
retrying==1.3.4
|
||||||
websockets==11.0.1
|
websockets==11.0.1
|
||||||
|
|
|
@ -74,7 +74,7 @@ def delete_tunnel(config):
|
||||||
|
|
||||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||||
def create_dns(config, hostname, type, content):
|
def create_dns(config, hostname, type, content):
|
||||||
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN"))
|
cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN"))
|
||||||
cf.zones.dns_records.post(
|
cf.zones.dns_records.post(
|
||||||
config["zone_tag"],
|
config["zone_tag"],
|
||||||
data={'name': hostname, 'type': type, 'content': content, 'proxied': True}
|
data={'name': hostname, 'type': type, 'content': content, 'proxied': True}
|
||||||
|
@ -89,7 +89,7 @@ def create_named_dns(config, random_uuid):
|
||||||
|
|
||||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||||
def delete_dns(config, hostname):
|
def delete_dns(config, hostname):
|
||||||
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN"))
|
cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN"))
|
||||||
zone_tag = config["zone_tag"]
|
zone_tag = config["zone_tag"]
|
||||||
dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname})
|
dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname})
|
||||||
if len(dns_records) > 0:
|
if len(dns_records) > 0:
|
||||||
|
|
|
@ -36,17 +36,17 @@ class TestConfig:
|
||||||
_ = start_cloudflared(tmp_path, config, validate_args)
|
_ = start_cloudflared(tmp_path, config, validate_args)
|
||||||
|
|
||||||
self.match_rule(tmp_path, config,
|
self.match_rule(tmp_path, config,
|
||||||
"http://example.com/index.html", 1)
|
"http://example.com/index.html", 0)
|
||||||
self.match_rule(tmp_path, config,
|
self.match_rule(tmp_path, config,
|
||||||
"https://example.com/index.html", 1)
|
"https://example.com/index.html", 0)
|
||||||
self.match_rule(tmp_path, config,
|
self.match_rule(tmp_path, config,
|
||||||
"https://api.example.com/login", 2)
|
"https://api.example.com/login", 1)
|
||||||
self.match_rule(tmp_path, config,
|
self.match_rule(tmp_path, config,
|
||||||
"https://wss.example.com", 3)
|
"https://wss.example.com", 2)
|
||||||
self.match_rule(tmp_path, config,
|
self.match_rule(tmp_path, config,
|
||||||
"https://ssh.example.com", 4)
|
"https://ssh.example.com", 3)
|
||||||
self.match_rule(tmp_path, config,
|
self.match_rule(tmp_path, config,
|
||||||
"https://api.example.com", 5)
|
"https://api.example.com", 4)
|
||||||
|
|
||||||
# This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. Note that rule number uses 1-based indexing
|
# This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. Note that rule number uses 1-based indexing
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import requests
|
||||||
|
from conftest import CfdModes
|
||||||
|
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||||
|
from retrying import retry
|
||||||
|
from cli import CloudflaredCli
|
||||||
|
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
|
||||||
|
import platform
|
||||||
|
|
||||||
|
"""
|
||||||
|
Each test in TestManagement will:
|
||||||
|
1. Acquire a management token from Cloudflare public API
|
||||||
|
2. Make a request against the management service for the running tunnel
|
||||||
|
"""
|
||||||
|
class TestManagement:
|
||||||
|
"""
|
||||||
|
test_get_host_details does the following:
|
||||||
|
1. It gets a management token from Tunnelstore using cloudflared tail token <tunnel_id>
|
||||||
|
2. It gets the connector_id after starting a cloudflare tunnel
|
||||||
|
3. It sends a request to the management host with the connector_id and management token
|
||||||
|
4. Asserts that the response has a hostname and ip.
|
||||||
|
"""
|
||||||
|
def test_get_host_details(self, tmp_path, component_tests_config):
|
||||||
|
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||||
|
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return
|
||||||
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||||
|
LOGGER.debug(config)
|
||||||
|
headers = {}
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
config_path = write_config(tmp_path, config.full_config)
|
||||||
|
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||||
|
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||||
|
require_min_connections=1)
|
||||||
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||||
|
connector_id = cfd_cli.get_connector_id(config)[0]
|
||||||
|
url = cfd_cli.get_management_url("host_details", config, config_path)
|
||||||
|
resp = send_request(url, headers=headers)
|
||||||
|
|
||||||
|
# Assert response json.
|
||||||
|
assert resp.status_code == 200, "Expected cloudflared to return 200 for host details"
|
||||||
|
assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname"
|
||||||
|
assert resp.json()["ip"] != "", "Expected cloudflared to return ip"
|
||||||
|
assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id"
|
||||||
|
|
||||||
|
"""
|
||||||
|
test_get_metrics will verify that the /metrics endpoint returns the prometheus metrics dump
|
||||||
|
"""
|
||||||
|
def test_get_metrics(self, tmp_path, component_tests_config):
|
||||||
|
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||||
|
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return
|
||||||
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||||
|
LOGGER.debug(config)
|
||||||
|
config_path = write_config(tmp_path, config.full_config)
|
||||||
|
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||||
|
wait_tunnel_ready(require_min_connections=1)
|
||||||
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||||
|
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||||
|
resp = send_request(url)
|
||||||
|
|
||||||
|
# Assert response.
|
||||||
|
assert resp.status_code == 200, "Expected cloudflared to return 200 for /metrics"
|
||||||
|
assert "# HELP build_info Build and version information" in resp.text, "Expected /metrics to have with the build_info details"
|
||||||
|
|
||||||
|
"""
|
||||||
|
test_get_pprof_heap will verify that the /debug/pprof/heap endpoint returns a pprof/heap dump response
|
||||||
|
"""
|
||||||
|
def test_get_pprof_heap(self, tmp_path, component_tests_config):
|
||||||
|
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||||
|
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return
|
||||||
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||||
|
LOGGER.debug(config)
|
||||||
|
config_path = write_config(tmp_path, config.full_config)
|
||||||
|
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
|
||||||
|
wait_tunnel_ready(require_min_connections=1)
|
||||||
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||||
|
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path)
|
||||||
|
resp = send_request(url)
|
||||||
|
|
||||||
|
# Assert response.
|
||||||
|
assert resp.status_code == 200, "Expected cloudflared to return 200 for /debug/pprof/heap"
|
||||||
|
assert resp.headers["Content-Type"] == "application/octet-stream", "Expected /debug/pprof/heap to have return a binary response"
|
||||||
|
|
||||||
|
"""
|
||||||
|
test_get_metrics_when_disabled will verify that diagnostic endpoints (such as /metrics) return 404 and are unmounted.
|
||||||
|
"""
|
||||||
|
def test_get_metrics_when_disabled(self, tmp_path, component_tests_config):
|
||||||
|
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
|
||||||
|
# Skipping this test for windows for now and will address it as part of tun-7377
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return
|
||||||
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||||
|
LOGGER.debug(config)
|
||||||
|
config_path = write_config(tmp_path, config.full_config)
|
||||||
|
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True):
|
||||||
|
wait_tunnel_ready(require_min_connections=1)
|
||||||
|
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
|
||||||
|
url = cfd_cli.get_management_url("metrics", config, config_path)
|
||||||
|
resp = send_request(url)
|
||||||
|
|
||||||
|
# Assert response.
|
||||||
|
assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics"
|
||||||
|
|
||||||
|
|
||||||
|
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||||
|
def send_request(url, headers={}):
|
||||||
|
with requests.Session() as s:
|
||||||
|
return s.get(url, timeout=BACKOFF_SECS, headers=headers)
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from conftest import CfdModes
|
from conftest import CfdModes
|
||||||
from constants import METRICS_PORT
|
from constants import METRICS_PORT
|
||||||
|
import time
|
||||||
from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_url, send_requests
|
from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_url, send_requests
|
||||||
|
|
||||||
class TestQuickTunnels:
|
class TestQuickTunnels:
|
||||||
|
@ -9,6 +10,7 @@ class TestQuickTunnels:
|
||||||
LOGGER.debug(config)
|
LOGGER.debug(config)
|
||||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
|
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
|
||||||
wait_tunnel_ready(require_min_connections=1)
|
wait_tunnel_ready(require_min_connections=1)
|
||||||
|
time.sleep(10)
|
||||||
url = get_quicktunnel_url()
|
url = get_quicktunnel_url()
|
||||||
send_requests(url, 3, True)
|
send_requests(url, 3, True)
|
||||||
|
|
||||||
|
@ -17,6 +19,7 @@ class TestQuickTunnels:
|
||||||
LOGGER.debug(config)
|
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):
|
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)
|
wait_tunnel_ready(require_min_connections=1)
|
||||||
|
time.sleep(10)
|
||||||
url = get_quicktunnel_url()
|
url = get_quicktunnel_url()
|
||||||
send_requests(url+"/ready", 3, True)
|
send_requests(url+"/ready", 3, True)
|
||||||
|
|
||||||
|
|
|
@ -16,38 +16,6 @@ class TestTunnel:
|
||||||
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
|
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
|
||||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||||
require_min_connections=1)
|
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):
|
def test_tunnel_url(self, tmp_path, component_tests_config):
|
||||||
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -14,8 +15,14 @@ from retrying import retry
|
||||||
|
|
||||||
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
def configure_logger():
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
LOGGER = configure_logger()
|
||||||
|
|
||||||
def select_platform(plat):
|
def select_platform(plat):
|
||||||
return pytest.mark.skipif(
|
return pytest.mark.skipif(
|
||||||
|
|
|
@ -205,6 +205,8 @@ type OriginRequestConfig struct {
|
||||||
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
|
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
|
||||||
// Hostname on the origin server certificate.
|
// Hostname on the origin server certificate.
|
||||||
OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"`
|
OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"`
|
||||||
|
// Auto configure the Hostname on the origin server certificate.
|
||||||
|
MatchSNIToHost *bool `yaml:"matchSNItoHost" json:"matchSNItoHost,omitempty"`
|
||||||
// Path to the CA for the certificate of your origin.
|
// Path to the CA for the certificate of your origin.
|
||||||
// This option should be used only if your certificate is not signed by Cloudflare.
|
// This option should be used only if your certificate is not signed by Cloudflare.
|
||||||
CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
|
CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
|
||||||
|
@ -257,7 +259,6 @@ type Configuration struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarpRoutingConfig struct {
|
type WarpRoutingConfig struct {
|
||||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
||||||
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
|
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
|
||||||
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
|
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ func TestConfigFileSettings(t *testing.T) {
|
||||||
Service: "https://localhost:8001",
|
Service: "https://localhost:8001",
|
||||||
}
|
}
|
||||||
warpRouting = WarpRoutingConfig{
|
warpRouting = WarpRoutingConfig{
|
||||||
Enabled: true,
|
|
||||||
ConnectTimeout: &CustomDuration{Duration: 2 * time.Second},
|
ConnectTimeout: &CustomDuration{Duration: 2 * time.Second},
|
||||||
TCPKeepAlive: &CustomDuration{Duration: 10 * time.Second},
|
TCPKeepAlive: &CustomDuration{Duration: 10 * time.Second},
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,6 @@ type Orchestrator interface {
|
||||||
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
|
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
|
||||||
GetConfigJSON() ([]byte, error)
|
GetConfigJSON() ([]byte, error)
|
||||||
GetOriginProxy() (OriginProxy, error)
|
GetOriginProxy() (OriginProxy, error)
|
||||||
WarpRoutingEnabled() (enabled bool)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NamedTunnelProperties struct {
|
type NamedTunnelProperties struct {
|
||||||
|
@ -157,14 +156,16 @@ type ReadWriteAcker interface {
|
||||||
type HTTPResponseReadWriteAcker struct {
|
type HTTPResponseReadWriteAcker struct {
|
||||||
r io.Reader
|
r io.Reader
|
||||||
w ResponseWriter
|
w ResponseWriter
|
||||||
|
f http.Flusher
|
||||||
req *http.Request
|
req *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
|
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
|
||||||
func NewHTTPResponseReadWriterAcker(w ResponseWriter, req *http.Request) *HTTPResponseReadWriteAcker {
|
func NewHTTPResponseReadWriterAcker(w ResponseWriter, flusher http.Flusher, req *http.Request) *HTTPResponseReadWriteAcker {
|
||||||
return &HTTPResponseReadWriteAcker{
|
return &HTTPResponseReadWriteAcker{
|
||||||
r: req.Body,
|
r: req.Body,
|
||||||
w: w,
|
w: w,
|
||||||
|
f: flusher,
|
||||||
req: req,
|
req: req,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,7 +175,11 @@ func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
|
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
|
||||||
return h.w.Write(p)
|
n, err := h.w.Write(p)
|
||||||
|
if n > 0 {
|
||||||
|
h.f.Flush()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
|
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
|
||||||
|
|
|
@ -130,7 +130,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
wsCtx, cancel := context.WithCancel(r.Context())
|
wsCtx, cancel := context.WithCancel(r.Context())
|
||||||
readPipe, writePipe := io.Pipe()
|
readPipe, writePipe := io.Pipe()
|
||||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
|
||||||
|
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case <-wsCtx.Done():
|
case <-wsCtx.Done():
|
||||||
|
@ -175,7 +176,7 @@ func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
wsCtx, cancel := context.WithCancel(r.Context())
|
wsCtx, cancel := context.WithCancel(r.Context())
|
||||||
|
|
||||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
|
||||||
|
|
||||||
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
|
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
|
||||||
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
|
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
|
||||||
|
|
|
@ -142,7 +142,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
|
rws := NewHTTPResponseReadWriterAcker(respWriter, respWriter, r)
|
||||||
requestErr = originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
requestErr = originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||||
Dest: host,
|
Dest: host,
|
||||||
CFRay: FindCfRayHeader(r),
|
CFRay: FindCfRayHeader(r),
|
||||||
|
@ -289,6 +289,10 @@ func (rp *http2RespWriter) Header() http.Header {
|
||||||
return rp.respHeaders
|
return rp.respHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rp *http2RespWriter) Flush() {
|
||||||
|
rp.flusher.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
func (rp *http2RespWriter) WriteHeader(status int) {
|
func (rp *http2RespWriter) WriteHeader(status int) {
|
||||||
if rp.hijacked() {
|
if rp.hijacked() {
|
||||||
rp.log.Warn().Msg("WriteHeader after hijack")
|
rp.log.Warn().Msg("WriteHeader after hijack")
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -84,7 +83,7 @@ func TestHTTP2ConfigurationSet(t *testing.T) {
|
||||||
resp, err := edgeHTTP2Conn.RoundTrip(req)
|
resp, err := edgeHTTP2Conn.RoundTrip(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
bdy, err := ioutil.ReadAll(resp.Body)
|
bdy, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy))
|
assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy))
|
||||||
cancel()
|
cancel()
|
||||||
|
@ -149,7 +148,7 @@ func TestServeHTTP(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.expectedStatus, resp.StatusCode)
|
require.Equal(t, test.expectedStatus, resp.StatusCode)
|
||||||
if test.expectedBody != nil {
|
if test.expectedBody != nil {
|
||||||
respBody, err := ioutil.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.expectedBody, respBody)
|
require.Equal(t, test.expectedBody, respBody)
|
||||||
}
|
}
|
||||||
|
@ -546,7 +545,7 @@ func benchmarkServeHTTP(b *testing.B, test testRequest) {
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
require.Equal(b, test.expectedStatus, resp.StatusCode)
|
require.Equal(b, test.expectedStatus, resp.StatusCode)
|
||||||
if test.expectedBody != nil {
|
if test.expectedBody != nil {
|
||||||
respBody, err := ioutil.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
require.Equal(b, test.expectedBody, respBody)
|
require.Equal(b, test.expectedBody, respBody)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -16,8 +17,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/quic-go/quic-go"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
@ -63,10 +64,14 @@ type QUICConnection struct {
|
||||||
controlStreamHandler ControlStreamHandler
|
controlStreamHandler ControlStreamHandler
|
||||||
connOptions *tunnelpogs.ConnectionOptions
|
connOptions *tunnelpogs.ConnectionOptions
|
||||||
connIndex uint8
|
connIndex uint8
|
||||||
|
|
||||||
|
udpUnregisterTimeout time.Duration
|
||||||
|
streamWriteTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewQUICConnection returns a new instance of QUICConnection.
|
// NewQUICConnection returns a new instance of QUICConnection.
|
||||||
func NewQUICConnection(
|
func NewQUICConnection(
|
||||||
|
ctx context.Context,
|
||||||
quicConfig *quic.Config,
|
quicConfig *quic.Config,
|
||||||
edgeAddr net.Addr,
|
edgeAddr net.Addr,
|
||||||
localAddr net.IP,
|
localAddr net.IP,
|
||||||
|
@ -77,13 +82,15 @@ func NewQUICConnection(
|
||||||
controlStreamHandler ControlStreamHandler,
|
controlStreamHandler ControlStreamHandler,
|
||||||
logger *zerolog.Logger,
|
logger *zerolog.Logger,
|
||||||
packetRouterConfig *ingress.GlobalRouterConfig,
|
packetRouterConfig *ingress.GlobalRouterConfig,
|
||||||
|
udpUnregisterTimeout time.Duration,
|
||||||
|
streamWriteTimeout time.Duration,
|
||||||
) (*QUICConnection, error) {
|
) (*QUICConnection, error) {
|
||||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, logger)
|
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := quic.Dial(udpConn, edgeAddr, edgeAddr.String(), tlsConfig, quicConfig)
|
session, err := quic.Dial(ctx, udpConn, edgeAddr, tlsConfig, quicConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// close the udp server socket in case of error connecting to the edge
|
// close the udp server socket in case of error connecting to the edge
|
||||||
udpConn.Close()
|
udpConn.Close()
|
||||||
|
@ -99,7 +106,7 @@ func NewQUICConnection(
|
||||||
sessionDemuxChan := make(chan *packet.Session, demuxChanCapacity)
|
sessionDemuxChan := make(chan *packet.Session, demuxChanCapacity)
|
||||||
datagramMuxer := quicpogs.NewDatagramMuxerV2(session, logger, sessionDemuxChan)
|
datagramMuxer := quicpogs.NewDatagramMuxerV2(session, logger, sessionDemuxChan)
|
||||||
sessionManager := datagramsession.NewManager(logger, datagramMuxer.SendToSession, sessionDemuxChan)
|
sessionManager := datagramsession.NewManager(logger, datagramMuxer.SendToSession, sessionDemuxChan)
|
||||||
packetRouter := ingress.NewPacketRouter(packetRouterConfig, datagramMuxer, logger, orchestrator.WarpRoutingEnabled)
|
packetRouter := ingress.NewPacketRouter(packetRouterConfig, datagramMuxer, logger)
|
||||||
|
|
||||||
return &QUICConnection{
|
return &QUICConnection{
|
||||||
session: session,
|
session: session,
|
||||||
|
@ -111,6 +118,8 @@ func NewQUICConnection(
|
||||||
controlStreamHandler: controlStreamHandler,
|
controlStreamHandler: controlStreamHandler,
|
||||||
connOptions: connOptions,
|
connOptions: connOptions,
|
||||||
connIndex: connIndex,
|
connIndex: connIndex,
|
||||||
|
udpUnregisterTimeout: udpUnregisterTimeout,
|
||||||
|
streamWriteTimeout: streamWriteTimeout,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +198,7 @@ func (q *QUICConnection) acceptStream(ctx context.Context) error {
|
||||||
|
|
||||||
func (q *QUICConnection) runStream(quicStream quic.Stream) {
|
func (q *QUICConnection) runStream(quicStream quic.Stream) {
|
||||||
ctx := quicStream.Context()
|
ctx := quicStream.Context()
|
||||||
stream := quicpogs.NewSafeStreamCloser(quicStream)
|
stream := quicpogs.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
|
|
||||||
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
|
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
|
||||||
|
@ -315,6 +324,7 @@ func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.
|
||||||
|
|
||||||
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
|
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
originProxy.Close()
|
||||||
log.Err(err).Str("sessionID", sessionID.String()).Msgf("Failed to register udp session")
|
log.Err(err).Str("sessionID", sessionID.String()).Msgf("Failed to register udp session")
|
||||||
tracing.EndWithErrorStatus(registerSpan, err)
|
tracing.EndWithErrorStatus(registerSpan, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -356,7 +366,7 @@ func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, close
|
||||||
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
|
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
|
||||||
func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) {
|
func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) {
|
||||||
q.sessionManager.UnregisterSession(ctx, sessionID, message, false)
|
q.sessionManager.UnregisterSession(ctx, sessionID, message, false)
|
||||||
stream, err := q.session.OpenStream()
|
quicStream, err := q.session.OpenStream()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||||
// with edge
|
// with edge
|
||||||
|
@ -366,7 +376,10 @@ func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUI
|
||||||
Msgf("Failed to open quic stream to unregister udp session with edge")
|
Msgf("Failed to open quic stream to unregister udp session with edge")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rpcClientStream, err := quicpogs.NewRPCClientStream(ctx, stream, q.logger)
|
|
||||||
|
stream := quicpogs.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
|
||||||
|
defer stream.Close()
|
||||||
|
rpcClientStream, err := quicpogs.NewRPCClientStream(ctx, stream, q.udpUnregisterTimeout, q.logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||||
// with edge
|
// with edge
|
||||||
|
@ -374,6 +387,8 @@ func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUI
|
||||||
Msgf("Failed to open rpc stream to unregister udp session with edge")
|
Msgf("Failed to open rpc stream to unregister udp session with edge")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer rpcClientStream.Close()
|
||||||
|
|
||||||
if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil {
|
if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil {
|
||||||
q.logger.Err(err).Str("sessionID", sessionID.String()).
|
q.logger.Err(err).Str("sessionID", sessionID.String()).
|
||||||
Msgf("Failed to unregister udp session with edge")
|
Msgf("Failed to unregister udp session with edge")
|
||||||
|
@ -439,10 +454,21 @@ func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header)
|
||||||
return hrw.WriteConnectResponseData(nil, metadata...)
|
return hrw.WriteConnectResponseData(nil, metadata...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hrw *httpResponseAdapter) Write(p []byte) (int, error) {
|
||||||
|
// Make sure to send WriteHeader response if not called yet
|
||||||
|
if !hrw.connectResponseSent {
|
||||||
|
hrw.WriteRespHeaders(http.StatusOK, hrw.headers)
|
||||||
|
}
|
||||||
|
return hrw.RequestServerStream.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
func (hrw *httpResponseAdapter) Header() http.Header {
|
func (hrw *httpResponseAdapter) Header() http.Header {
|
||||||
return hrw.headers
|
return hrw.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a no-op Flush because this adapter is over a quic.Stream and we don't need Flush here.
|
||||||
|
func (hrw *httpResponseAdapter) Flush() {}
|
||||||
|
|
||||||
func (hrw *httpResponseAdapter) WriteHeader(status int) {
|
func (hrw *httpResponseAdapter) WriteHeader(status int) {
|
||||||
hrw.WriteRespHeaders(status, hrw.headers)
|
hrw.WriteRespHeaders(status, hrw.headers)
|
||||||
}
|
}
|
||||||
|
@ -602,9 +628,19 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, logger *zerolog.
|
||||||
localIP = net.IPv4zero
|
localIP = net.IPv4zero
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listenNetwork := "udp"
|
||||||
|
// https://github.com/quic-go/quic-go/issues/3793 DF bit cannot be set for dual stack listener on OSX
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
if localIP.To4() != nil {
|
||||||
|
listenNetwork = "udp4"
|
||||||
|
} else {
|
||||||
|
listenNetwork = "udp6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if port was not set yet, it will be zero, so bind will randomly allocate one.
|
// if port was not set yet, it will be zero, so bind will randomly allocate one.
|
||||||
if port, ok := portForConnIndex[connIndex]; ok {
|
if port, ok := portForConnIndex[connIndex]; ok {
|
||||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP, Port: port})
|
udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: port})
|
||||||
// if there wasn't an error, or if port was 0 (independently of error or not, just return)
|
// if there wasn't an error, or if port was 0 (independently of error or not, just return)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return udpConn, nil
|
return udpConn, nil
|
||||||
|
@ -614,7 +650,7 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, logger *zerolog.
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we reached here, then there was an error or port as not been allocated it.
|
// 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})
|
udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr)
|
udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
@ -16,8 +16,8 @@ import (
|
||||||
|
|
||||||
"github.com/gobwas/ws/wsutil"
|
"github.com/gobwas/ws/wsutil"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/quic-go/quic-go"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -32,10 +32,10 @@ import (
|
||||||
var (
|
var (
|
||||||
testTLSServerConfig = quicpogs.GenerateTLSConfig()
|
testTLSServerConfig = quicpogs.GenerateTLSConfig()
|
||||||
testQUICConfig = &quic.Config{
|
testQUICConfig = &quic.Config{
|
||||||
ConnectionIDLength: 16,
|
KeepAlivePeriod: 5 * time.Second,
|
||||||
KeepAlivePeriod: 5 * time.Second,
|
EnableDatagrams: true,
|
||||||
EnableDatagrams: true,
|
|
||||||
}
|
}
|
||||||
|
defaultQUICTimeout = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ ReadWriteAcker = (*streamReadWriteAcker)(nil)
|
var _ ReadWriteAcker = (*streamReadWriteAcker)(nil)
|
||||||
|
@ -43,13 +43,6 @@ var _ ReadWriteAcker = (*streamReadWriteAcker)(nil)
|
||||||
// TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol.
|
// TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol.
|
||||||
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
|
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
|
||||||
func TestQUICServer(t *testing.T) {
|
func TestQUICServer(t *testing.T) {
|
||||||
// Start a UDP Listener for QUIC.
|
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
|
||||||
require.NoError(t, err)
|
|
||||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer udpListener.Close()
|
|
||||||
|
|
||||||
// This is simply a sample websocket frame message.
|
// This is simply a sample websocket frame message.
|
||||||
wsBuf := &bytes.Buffer{}
|
wsBuf := &bytes.Buffer{}
|
||||||
wsutil.WriteClientBinary(wsBuf, []byte("Hello"))
|
wsutil.WriteClientBinary(wsBuf, []byte("Hello"))
|
||||||
|
@ -145,8 +138,14 @@ func TestQUICServer(t *testing.T) {
|
||||||
test := test // capture range variable
|
test := test // capture range variable
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
// Start a UDP Listener for QUIC.
|
||||||
quicListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig)
|
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer udpListener.Close()
|
||||||
|
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
|
||||||
|
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
serverDone := make(chan struct{})
|
serverDone := make(chan struct{})
|
||||||
|
@ -187,7 +186,7 @@ func (fakeControlStream) IsStopped() bool {
|
||||||
func quicServer(
|
func quicServer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
listener quic.Listener,
|
listener *quic.Listener,
|
||||||
dest string,
|
dest string,
|
||||||
connectionType quicpogs.ConnectionType,
|
connectionType quicpogs.ConnectionType,
|
||||||
metadata []quicpogs.Metadata,
|
metadata []quicpogs.Metadata,
|
||||||
|
@ -199,7 +198,7 @@ func quicServer(
|
||||||
|
|
||||||
quicStream, err := session.OpenStreamSync(context.Background())
|
quicStream, err := session.OpenStreamSync(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
stream := quicpogs.NewSafeStreamCloser(quicStream)
|
stream := quicpogs.NewSafeStreamCloser(quicStream, defaultQUICTimeout, &log)
|
||||||
|
|
||||||
reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream}
|
reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream}
|
||||||
err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...)
|
err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...)
|
||||||
|
@ -623,7 +622,7 @@ func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.
|
||||||
muxedPayload, err = quicpogs.SuffixType(muxedPayload, quicpogs.DatagramTypeUDP)
|
muxedPayload, err = quicpogs.SuffixType(muxedPayload, quicpogs.DatagramTypeUDP)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = edgeQUICSession.SendMessage(muxedPayload)
|
err = edgeQUICSession.SendDatagram(muxedPayload)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
readBuffer := make([]byte, len(payload)+1)
|
readBuffer := make([]byte, len(payload)+1)
|
||||||
|
@ -713,7 +712,10 @@ func testQUICConnection(udpListenerAddr net.Addr, t *testing.T, index uint8) *QU
|
||||||
}
|
}
|
||||||
// Start a mock httpProxy
|
// Start a mock httpProxy
|
||||||
log := zerolog.New(os.Stdout)
|
log := zerolog.New(os.Stdout)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
qc, err := NewQUICConnection(
|
qc, err := NewQUICConnection(
|
||||||
|
ctx,
|
||||||
testQUICConfig,
|
testQUICConfig,
|
||||||
udpListenerAddr,
|
udpListenerAddr,
|
||||||
nil,
|
nil,
|
||||||
|
@ -724,6 +726,8 @@ func testQUICConnection(udpListenerAddr net.Addr, t *testing.T, index uint8) *QU
|
||||||
fakeControlStream{},
|
fakeControlStream{},
|
||||||
&log,
|
&log,
|
||||||
nil,
|
nil,
|
||||||
|
5*time.Second,
|
||||||
|
0*time.Second,
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return qc
|
return qc
|
||||||
|
|
|
@ -92,7 +92,10 @@ func (m *manager) shutdownSessions(err error) {
|
||||||
byRemote: true,
|
byRemote: true,
|
||||||
}
|
}
|
||||||
for _, s := range m.sessions {
|
for _, s := range m.sessions {
|
||||||
s.close(closeSessionErr)
|
m.unregisterSession(&unregisterSessionEvent{
|
||||||
|
sessionID: s.ID,
|
||||||
|
err: closeSessionErr,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +121,7 @@ func (m *manager) registerSession(ctx context.Context, registration *registerSes
|
||||||
session := m.newSession(registration.sessionID, registration.originProxy)
|
session := m.newSession(registration.sessionID, registration.originProxy)
|
||||||
m.sessions[registration.sessionID] = session
|
m.sessions[registration.sessionID] = session
|
||||||
registration.resultChan <- session
|
registration.resultChan <- session
|
||||||
|
incrementUDPSessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manager) newSession(id uuid.UUID, dstConn io.ReadWriteCloser) *Session {
|
func (m *manager) newSession(id uuid.UUID, dstConn io.ReadWriteCloser) *Session {
|
||||||
|
@ -163,6 +167,7 @@ func (m *manager) unregisterSession(unregistration *unregisterSessionEvent) {
|
||||||
if ok {
|
if ok {
|
||||||
delete(m.sessions, unregistration.sessionID)
|
delete(m.sessions, unregistration.sessionID)
|
||||||
session.close(unregistration.err)
|
session.close(unregistration.err)
|
||||||
|
decrementUDPActiveSessions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package datagramsession
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
namespace = "cloudflared"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
activeUDPSessions = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "udp",
|
||||||
|
Name: "active_sessions",
|
||||||
|
Help: "Concurrent count of UDP sessions that are being proxied to any origin",
|
||||||
|
})
|
||||||
|
totalUDPSessions = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: "udp",
|
||||||
|
Name: "total_sessions",
|
||||||
|
Help: "Total count of UDP sessions that have been proxied to any origin",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
prometheus.MustRegister(
|
||||||
|
activeUDPSessions,
|
||||||
|
totalUDPSessions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func incrementUDPSessions() {
|
||||||
|
totalUDPSessions.Inc()
|
||||||
|
activeUDPSessions.Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrementUDPActiveSessions() {
|
||||||
|
activeUDPSessions.Dec()
|
||||||
|
}
|
|
@ -51,7 +51,7 @@ type Session struct {
|
||||||
|
|
||||||
func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) (closedByRemote bool, err error) {
|
func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) (closedByRemote bool, err error) {
|
||||||
go func() {
|
go func() {
|
||||||
// QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
// QUIC implementation copies data to another buffer before returning https://github.com/quic-go/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
||||||
// This makes it safe to share readBuffer between iterations
|
// This makes it safe to share readBuffer between iterations
|
||||||
const maxPacketSize = 1500
|
const maxPacketSize = 1500
|
||||||
readBuffer := make([]byte, maxPacketSize)
|
readBuffer := make([]byte, maxPacketSize)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
FROM golang:1.19 as builder
|
FROM golang:1.21.5 as builder
|
||||||
ENV GO111MODULE=on \
|
ENV GO111MODULE=on \
|
||||||
CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN .teamcity/install-cloudflare-go.sh
|
||||||
# compile cloudflared
|
# compile cloudflared
|
||||||
RUN make cloudflared
|
RUN PATH="/tmp/go/bin:$PATH" make cloudflared
|
||||||
RUN cp /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
RUN cp /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
|
||||||
|
|
|
@ -28,3 +28,22 @@ func Contains(feature string) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove any duplicates from the slice
|
||||||
|
func Dedup(slice []string) []string {
|
||||||
|
|
||||||
|
// Convert the slice into a set
|
||||||
|
set := make(map[string]bool, 0)
|
||||||
|
for _, str := range slice {
|
||||||
|
set[str] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the set back into a slice
|
||||||
|
keys := make([]string, len(set))
|
||||||
|
i := 0
|
||||||
|
for str := range set {
|
||||||
|
keys[i] = str
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package features
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
featureSelectorHostname = "cfd-features.argotunnel.com"
|
||||||
|
defaultRefreshFreq = time.Hour * 6
|
||||||
|
lookupTimeout = time.Second * 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostQuantumMode uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Prefer post quantum, but fallback if connection cannot be established
|
||||||
|
PostQuantumPrefer PostQuantumMode = iota
|
||||||
|
// If the user passes the --post-quantum flag, we override
|
||||||
|
// CurvePreferences to only support hybrid post-quantum key agreements.
|
||||||
|
PostQuantumStrict
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the TXT record adds other fields, the umarshal logic will ignore those keys
|
||||||
|
// If the TXT record is missing a key, the field will unmarshal to the default Go value
|
||||||
|
// pq was removed in TUN-7970
|
||||||
|
type featuresRecord struct{}
|
||||||
|
|
||||||
|
func NewFeatureSelector(ctx context.Context, accountTag string, staticFeatures StaticFeatures, logger *zerolog.Logger) (*FeatureSelector, error) {
|
||||||
|
return newFeatureSelector(ctx, accountTag, logger, newDNSResolver(), staticFeatures, defaultRefreshFreq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeatureSelector determines if this account will try new features. It preiodically queries a DNS TXT record
|
||||||
|
// to see which features are turned on
|
||||||
|
type FeatureSelector struct {
|
||||||
|
accountHash int32
|
||||||
|
logger *zerolog.Logger
|
||||||
|
resolver resolver
|
||||||
|
|
||||||
|
staticFeatures StaticFeatures
|
||||||
|
|
||||||
|
// lock protects concurrent access to dynamic features
|
||||||
|
lock sync.RWMutex
|
||||||
|
features featuresRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
// Features set by user provided flags
|
||||||
|
type StaticFeatures struct {
|
||||||
|
PostQuantumMode *PostQuantumMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFeatureSelector(ctx context.Context, accountTag string, logger *zerolog.Logger, resolver resolver, staticFeatures StaticFeatures, refreshFreq time.Duration) (*FeatureSelector, error) {
|
||||||
|
selector := &FeatureSelector{
|
||||||
|
accountHash: switchThreshold(accountTag),
|
||||||
|
logger: logger,
|
||||||
|
resolver: resolver,
|
||||||
|
staticFeatures: staticFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := selector.refresh(ctx); err != nil {
|
||||||
|
logger.Err(err).Msg("Failed to fetch features, default to disable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run refreshLoop next time we have a new feature to rollout
|
||||||
|
|
||||||
|
return selector, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FeatureSelector) PostQuantumMode() PostQuantumMode {
|
||||||
|
if fs.staticFeatures.PostQuantumMode != nil {
|
||||||
|
return *fs.staticFeatures.PostQuantumMode
|
||||||
|
}
|
||||||
|
|
||||||
|
return PostQuantumPrefer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FeatureSelector) refreshLoop(ctx context.Context, refreshFreq time.Duration) {
|
||||||
|
ticker := time.NewTicker(refreshFreq)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
err := fs.refresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fs.logger.Err(err).Msg("Failed to refresh feature selector")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FeatureSelector) refresh(ctx context.Context) error {
|
||||||
|
record, err := fs.resolver.lookupRecord(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var features featuresRecord
|
||||||
|
if err := json.Unmarshal(record, &features); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.lock.Lock()
|
||||||
|
defer fs.lock.Unlock()
|
||||||
|
|
||||||
|
fs.features = features
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolver represents an object that can look up featuresRecord
|
||||||
|
type resolver interface {
|
||||||
|
lookupRecord(ctx context.Context) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsResolver struct {
|
||||||
|
resolver *net.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDNSResolver() *dnsResolver {
|
||||||
|
return &dnsResolver{
|
||||||
|
resolver: net.DefaultResolver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, lookupTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
records, err := dr.resolver.LookupTXT(ctx, featureSelectorHostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, fmt.Errorf("No TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(records[0]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func switchThreshold(accountTag string) int32 {
|
||||||
|
h := fnv.New32a()
|
||||||
|
_, _ = h.Write([]byte(accountTag))
|
||||||
|
return int32(h.Sum32() % 100)
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package features
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalFeaturesRecord(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
record []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
record: []byte(`{"pq":0}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
record: []byte(`{"pq":39}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
record: []byte(`{"pq":100}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
record: []byte(`{}`), // Unmarshal to default struct if key is not present
|
||||||
|
},
|
||||||
|
{
|
||||||
|
record: []byte(`{"kyber":768}`), // Unmarshal to default struct if key is not present
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
var features featuresRecord
|
||||||
|
err := json.Unmarshal(test.record, &features)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, featuresRecord{}, features)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticFeatures(t *testing.T) {
|
||||||
|
pqMode := PostQuantumStrict
|
||||||
|
selector := newTestSelector(t, &pqMode, time.Millisecond*10)
|
||||||
|
require.Equal(t, PostQuantumStrict, selector.PostQuantumMode())
|
||||||
|
|
||||||
|
// No StaticFeatures configured
|
||||||
|
selector = newTestSelector(t, nil, time.Millisecond*10)
|
||||||
|
require.Equal(t, PostQuantumPrefer, selector.PostQuantumMode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestSelector(t *testing.T, pqMode *PostQuantumMode, refreshFreq time.Duration) *FeatureSelector {
|
||||||
|
accountTag := t.Name()
|
||||||
|
logger := zerolog.Nop()
|
||||||
|
|
||||||
|
resolver := &mockResolver{}
|
||||||
|
|
||||||
|
staticFeatures := StaticFeatures{
|
||||||
|
PostQuantumMode: pqMode,
|
||||||
|
}
|
||||||
|
selector, err := newFeatureSelector(context.Background(), accountTag, &logger, resolver, staticFeatures, refreshFreq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return selector
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockResolver struct{}
|
||||||
|
|
||||||
|
func (mr *mockResolver) lookupRecord(ctx context.Context) ([]byte, error) {
|
||||||
|
return nil, fmt.Errorf("mockResolver hasn't implement lookupRecord")
|
||||||
|
}
|
99
go.mod
99
go.mod
|
@ -1,51 +1,48 @@
|
||||||
module github.com/cloudflare/cloudflared
|
module github.com/cloudflare/cloudflared
|
||||||
|
|
||||||
go 1.19
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
|
|
||||||
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
|
|
||||||
github.com/coredns/coredns v1.10.0
|
github.com/coredns/coredns v1.10.0
|
||||||
github.com/coreos/go-oidc/v3 v3.4.0
|
github.com/coreos/go-oidc/v3 v3.10.0
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd/v22 v22.5.0
|
||||||
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
|
||||||
|
github.com/fortytw2/leaktest v1.3.0
|
||||||
github.com/fsnotify/fsnotify v1.4.9
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
github.com/getsentry/raven-go v0.2.0
|
|
||||||
github.com/getsentry/sentry-go v0.16.0
|
github.com/getsentry/sentry-go v0.16.0
|
||||||
github.com/go-chi/chi/v5 v5.0.8
|
github.com/go-chi/chi/v5 v5.0.8
|
||||||
github.com/go-jose/go-jose/v3 v3.0.0
|
github.com/go-chi/cors v1.2.1
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.1
|
||||||
github.com/gobwas/ws v1.0.4
|
github.com/gobwas/ws v1.0.4
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.1
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/lucas-clemente/quic-go v0.28.1
|
|
||||||
github.com/mattn/go-colorable v0.1.13
|
github.com/mattn/go-colorable v0.1.13
|
||||||
github.com/miekg/dns v1.1.50
|
github.com/miekg/dns v1.1.50
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.13.0
|
github.com/prometheus/client_golang v1.13.0
|
||||||
github.com/prometheus/client_model v0.2.0
|
github.com/prometheus/client_model v0.2.0
|
||||||
|
github.com/quic-go/quic-go v0.42.0
|
||||||
github.com/rs/zerolog v1.20.0
|
github.com/rs/zerolog v1.20.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/urfave/cli/v2 v2.3.0
|
github.com/urfave/cli/v2 v2.3.0
|
||||||
go.opentelemetry.io/contrib/propagators v0.22.0
|
go.opentelemetry.io/contrib/propagators v0.22.0
|
||||||
go.opentelemetry.io/otel v1.6.3
|
go.opentelemetry.io/otel v1.21.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.3
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0
|
||||||
go.opentelemetry.io/otel/sdk v1.6.3
|
go.opentelemetry.io/otel/sdk v1.21.0
|
||||||
go.opentelemetry.io/otel/trace v1.6.3
|
go.opentelemetry.io/otel/trace v1.21.0
|
||||||
go.opentelemetry.io/proto/otlp v0.15.0
|
go.opentelemetry.io/proto/otlp v1.0.0
|
||||||
go.uber.org/automaxprocs v1.4.0
|
go.uber.org/automaxprocs v1.4.0
|
||||||
golang.org/x/crypto v0.8.0
|
golang.org/x/crypto v0.21.0
|
||||||
golang.org/x/net v0.9.0
|
golang.org/x/net v0.21.0
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.4.0
|
||||||
golang.org/x/sys v0.7.0
|
golang.org/x/sys v0.18.0
|
||||||
golang.org/x/term v0.7.0
|
golang.org/x/term v0.18.0
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/coreos/go-oidc.v2 v2.2.1
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
nhooyr.io/websocket v1.8.7
|
nhooyr.io/websocket v1.8.7
|
||||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
||||||
|
@ -55,10 +52,7 @@ require (
|
||||||
github.com/BurntSushi/toml v1.2.0 // indirect
|
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
|
||||||
github.com/cheekybits/genny v1.0.0 // indirect
|
|
||||||
github.com/cloudflare/circl v1.2.1-0.20220809205628-0a9554f37a47 // indirect
|
|
||||||
github.com/coredns/caddy v1.1.1 // indirect
|
github.com/coredns/caddy v1.1.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
@ -67,62 +61,45 @@ require (
|
||||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // indirect
|
github.com/go-logr/logr v1.3.0 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 // indirect
|
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||||
github.com/klauspost/compress v1.15.11 // indirect
|
github.com/klauspost/compress v1.15.11 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
|
||||||
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
|
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
|
||||||
github.com/onsi/gomega v1.23.0 // indirect
|
|
||||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||||
golang.org/x/oauth2 v0.4.0 // indirect
|
go.uber.org/mock v0.4.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/mod v0.11.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
golang.org/x/oauth2 v0.13.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
google.golang.org/grpc v1.51.0 // indirect
|
golang.org/x/tools v0.9.1 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect
|
||||||
|
google.golang.org/grpc v1.60.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
||||||
|
|
||||||
replace github.com/lucas-clemente/quic-go => github.com/chungthuang/quic-go v0.27.1-0.20220809135021-ca330f1dec9f
|
|
||||||
|
|
||||||
// Avoid 'CVE-2022-21698'
|
// Avoid 'CVE-2022-21698'
|
||||||
replace github.com/prometheus/golang_client => github.com/prometheus/golang_client v1.12.1
|
replace github.com/prometheus/golang_client => github.com/prometheus/golang_client v1.12.1
|
||||||
|
|
||||||
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
||||||
// Post-quantum tunnel RTG-1339
|
|
||||||
replace (
|
|
||||||
// Branches go1.18 go1.19 go1.20 on github.com/cloudflare/qtls-pq
|
|
||||||
github.com/marten-seemann/qtls-go1-18 => github.com/cloudflare/qtls-pq v0.0.0-20230103171413-e7a2fb559a0e
|
|
||||||
github.com/marten-seemann/qtls-go1-19 => github.com/cloudflare/qtls-pq v0.0.0-20230103171656-05e84f90909e
|
|
||||||
github.com/marten-seemann/qtls-go1-20 => github.com/cloudflare/qtls-pq v0.0.0-20230215110727-8b4e1699c2a8
|
|
||||||
github.com/quic-go/qtls-go1-18 => github.com/cloudflare/qtls-pq v0.0.0-20230103171413-e7a2fb559a0e
|
|
||||||
github.com/quic-go/qtls-go1-19 => github.com/cloudflare/qtls-pq v0.0.0-20230103171656-05e84f90909e
|
|
||||||
github.com/quic-go/qtls-go1-20 => github.com/cloudflare/qtls-pq v0.0.0-20230215110727-8b4e1699c2a8
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
//go:build !cgo
|
|
||||||
// +build !cgo
|
|
||||||
|
|
||||||
package h2mux
|
package h2mux
|
||||||
|
|
||||||
import (
|
import (
|
|
@ -1,22 +0,0 @@
|
||||||
//go:build cgo
|
|
||||||
// +build cgo
|
|
||||||
|
|
||||||
package h2mux
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/cloudflare/brotli-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CompressionIsSupported() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDecompressor(src io.Reader) *brotli.Reader {
|
|
||||||
return brotli.NewReader(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCompressor(dst io.Writer, quality, lgwin int) *brotli.Writer {
|
|
||||||
return brotli.NewWriter(dst, brotli.WriterOptions{Quality: quality, LGWin: lgwin})
|
|
||||||
}
|
|
|
@ -135,14 +135,7 @@ func Handshake(
|
||||||
m.f.ReadMetaHeaders = hpack.NewDecoder(4096, func(hpack.HeaderField) {})
|
m.f.ReadMetaHeaders = hpack.NewDecoder(4096, func(hpack.HeaderField) {})
|
||||||
// Initialise the settings to identify this connection and confirm the other end is sane.
|
// Initialise the settings to identify this connection and confirm the other end is sane.
|
||||||
handshakeSetting := http2.Setting{ID: SettingMuxerMagic, Val: MuxerMagicEdge}
|
handshakeSetting := http2.Setting{ID: SettingMuxerMagic, Val: MuxerMagicEdge}
|
||||||
compressionSetting := http2.Setting{ID: SettingCompression, Val: config.CompressionQuality.toH2Setting()}
|
compressionSetting := http2.Setting{ID: SettingCompression, Val: 0}
|
||||||
if CompressionIsSupported() {
|
|
||||||
config.Log.Debug().Msg("muxer: Compression is supported")
|
|
||||||
m.compressionQuality = config.CompressionQuality.getPreset()
|
|
||||||
} else {
|
|
||||||
config.Log.Debug().Msg("muxer: Compression is not supported")
|
|
||||||
compressionSetting = http2.Setting{ID: SettingCompression, Val: 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedMagic := MuxerMagicOrigin
|
expectedMagic := MuxerMagicOrigin
|
||||||
if config.IsClient {
|
if config.IsClient {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
@ -652,7 +651,7 @@ func TestHPACK(t *testing.T) {
|
||||||
if stream.Headers[0].Value != "200" {
|
if stream.Headers[0].Value != "200" {
|
||||||
t.Fatalf("expected status 200, got %s", stream.Headers[0].Value)
|
t.Fatalf("expected status 200, got %s", stream.Headers[0].Value)
|
||||||
}
|
}
|
||||||
_, _ = ioutil.ReadAll(stream)
|
_, _ = io.ReadAll(stream)
|
||||||
_ = stream.Close()
|
_ = stream.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -678,157 +677,6 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultipleStreamsWithDictionaries(t *testing.T) {
|
|
||||||
l := zerolog.Nop()
|
|
||||||
|
|
||||||
for q := CompressionNone; q <= CompressionMax; q++ {
|
|
||||||
htmlBody := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"` +
|
|
||||||
`"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">` +
|
|
||||||
`<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">` +
|
|
||||||
`<head>` +
|
|
||||||
` <title>Your page title here</title>` +
|
|
||||||
`</head>` +
|
|
||||||
`<body>` +
|
|
||||||
`<h1>Your major heading here</h1>` +
|
|
||||||
`<p>` +
|
|
||||||
`This is a regular text paragraph.` +
|
|
||||||
`</p>` +
|
|
||||||
`<ul>` +
|
|
||||||
` <li>` +
|
|
||||||
` First bullet of a bullet list.` +
|
|
||||||
` </li>` +
|
|
||||||
` <li>` +
|
|
||||||
` This is the <em>second</em> bullet.` +
|
|
||||||
` </li>` +
|
|
||||||
`</ul>` +
|
|
||||||
`</body>` +
|
|
||||||
`</html>`
|
|
||||||
|
|
||||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
|
||||||
var contentType string
|
|
||||||
var pathHeader Header
|
|
||||||
|
|
||||||
for _, h := range stream.Headers {
|
|
||||||
if h.Name == ":path" {
|
|
||||||
pathHeader = h
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pathHeader.Name != ":path" {
|
|
||||||
panic("Couldn't find :path header in test")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(pathHeader.Value, "html") {
|
|
||||||
contentType = "text/html; charset=utf-8"
|
|
||||||
} else if strings.Contains(pathHeader.Value, "js") {
|
|
||||||
contentType = "application/javascript"
|
|
||||||
} else if strings.Contains(pathHeader.Value, "css") {
|
|
||||||
contentType = "text/css"
|
|
||||||
} else {
|
|
||||||
contentType = "img/gif"
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = stream.WriteHeaders([]Header{
|
|
||||||
{Name: "content-type", Value: contentType},
|
|
||||||
})
|
|
||||||
_, _ = stream.Write([]byte(strings.Replace(htmlBody, "paragraph", pathHeader.Value, 1) + stream.Headers[5].Value))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, f)
|
|
||||||
muxPair.Serve(t)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
paths := []string{
|
|
||||||
"/html1",
|
|
||||||
"/html2?sa:ds",
|
|
||||||
"/html3",
|
|
||||||
"/css1",
|
|
||||||
"/html1",
|
|
||||||
"/html2?sa:ds",
|
|
||||||
"/html3",
|
|
||||||
"/css1",
|
|
||||||
"/css2",
|
|
||||||
"/css3",
|
|
||||||
"/js",
|
|
||||||
"/js",
|
|
||||||
"/js",
|
|
||||||
"/js2",
|
|
||||||
"/img2",
|
|
||||||
"/html1",
|
|
||||||
"/html2?sa:ds",
|
|
||||||
"/html3",
|
|
||||||
"/css1",
|
|
||||||
"/css2",
|
|
||||||
"/css3",
|
|
||||||
"/js",
|
|
||||||
"/js",
|
|
||||||
"/js",
|
|
||||||
"/js2",
|
|
||||||
"/img1",
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(len(paths))
|
|
||||||
errorsC := make(chan error, len(paths))
|
|
||||||
|
|
||||||
for i, s := range paths {
|
|
||||||
go func(index int, path string) {
|
|
||||||
defer wg.Done()
|
|
||||||
stream, err := muxPair.OpenEdgeMuxStream(
|
|
||||||
[]Header{
|
|
||||||
{Name: ":method", Value: "GET"},
|
|
||||||
{Name: ":scheme", Value: "https"},
|
|
||||||
{Name: ":authority", Value: "tunnel.otterlyadorable.co.uk"},
|
|
||||||
{Name: ":path", Value: path},
|
|
||||||
{Name: "cf-ray", Value: "378948953f044408-SFO-DOG"},
|
|
||||||
{Name: "idx", Value: strconv.Itoa(index)},
|
|
||||||
{Name: "accept-encoding", Value: "gzip, br"},
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
errorsC <- fmt.Errorf("error in OpenStream: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expectBody := strings.Replace(htmlBody, "paragraph", path, 1) + strconv.Itoa(index)
|
|
||||||
responseBody := make([]byte, len(expectBody)*2)
|
|
||||||
n, err := stream.Read(responseBody)
|
|
||||||
if err != nil {
|
|
||||||
errorsC <- fmt.Errorf("stream %d error from (*MuxedStream).Read: %s", stream.streamID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n != len(expectBody) {
|
|
||||||
errorsC <- fmt.Errorf("stream %d expected response body to have %d bytes, got %d", stream.streamID, len(expectBody), n)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if string(responseBody[:n]) != expectBody {
|
|
||||||
errorsC <- fmt.Errorf("stream %d expected response body %s, got %s", stream.streamID, expectBody, responseBody[:n])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(i, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(errorsC)
|
|
||||||
testFail := false
|
|
||||||
for err := range errorsC {
|
|
||||||
testFail = true
|
|
||||||
l.Error().Msgf("%s", err)
|
|
||||||
}
|
|
||||||
if testFail {
|
|
||||||
t.Fatalf("TestMultipleStreams failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
|
||||||
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
|
|
||||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
|
func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
|
||||||
return func(stream *MuxedStream) error {
|
return func(stream *MuxedStream) error {
|
||||||
var contentType string
|
var contentType string
|
||||||
|
@ -905,7 +753,7 @@ func loadSampleFiles(paths []string) (map[string][]byte, error) {
|
||||||
files := make(map[string][]byte)
|
files := make(map[string][]byte)
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
if _, ok := files[path]; !ok {
|
if _, ok := files[path]; !ok {
|
||||||
expectBody, err := ioutil.ReadFile(path)
|
expectBody, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -915,82 +763,6 @@ func loadSampleFiles(paths []string) (map[string][]byte, error) {
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSampleSiteWithDictionaries(t *testing.T) {
|
|
||||||
paths := []string{
|
|
||||||
"./sample/index.html",
|
|
||||||
"./sample/index2.html",
|
|
||||||
"./sample/index1.html",
|
|
||||||
"./sample/ghost-url.min.js",
|
|
||||||
"./sample/jquery.fitvids.js",
|
|
||||||
"./sample/index1.html",
|
|
||||||
"./sample/index2.html",
|
|
||||||
"./sample/index.html",
|
|
||||||
}
|
|
||||||
files, err := loadSampleFiles(paths)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
for q := CompressionNone; q <= CompressionMax; q++ {
|
|
||||||
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, sampleSiteHandler(files))
|
|
||||||
muxPair.Serve(t)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errC := make(chan error, len(paths))
|
|
||||||
|
|
||||||
wg.Add(len(paths))
|
|
||||||
for _, s := range paths {
|
|
||||||
go func(path string) {
|
|
||||||
defer wg.Done()
|
|
||||||
errC <- sampleSiteTest(muxPair, path, files)
|
|
||||||
}(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(errC)
|
|
||||||
|
|
||||||
for err := range errC {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
|
||||||
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
|
|
||||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLongSiteWithDictionaries(t *testing.T) {
|
|
||||||
paths := []string{
|
|
||||||
"./sample/index.html",
|
|
||||||
"./sample/index1.html",
|
|
||||||
"./sample/index2.html",
|
|
||||||
"./sample/ghost-url.min.js",
|
|
||||||
"./sample/jquery.fitvids.js",
|
|
||||||
}
|
|
||||||
files, err := loadSampleFiles(paths)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
for q := CompressionNone; q <= CompressionMedium; q++ {
|
|
||||||
muxPair := NewCompressedMuxerPair(t, fmt.Sprintf("%s_%d", t.Name(), q), q, sampleSiteHandler(files))
|
|
||||||
muxPair.Serve(t)
|
|
||||||
|
|
||||||
rand.Seed(time.Now().Unix())
|
|
||||||
|
|
||||||
tstLen := 500
|
|
||||||
errGroup, _ := errgroup.WithContext(context.Background())
|
|
||||||
for i := 0; i < tstLen; i++ {
|
|
||||||
errGroup.Go(func() error {
|
|
||||||
path := paths[rand.Int()%len(paths)]
|
|
||||||
return sampleSiteTest(muxPair, path, files)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
assert.NoError(t, errGroup.Wait())
|
|
||||||
|
|
||||||
originMuxMetrics := muxPair.OriginMux.Metrics()
|
|
||||||
if q > CompressionNone && originMuxMetrics.CompBytesBefore.Value() <= 10*originMuxMetrics.CompBytesAfter.Value() {
|
|
||||||
t.Fatalf("Cross-stream compression is expected to give a better compression ratio")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkOpenStream(b *testing.B) {
|
func BenchmarkOpenStream(b *testing.B) {
|
||||||
const streams = 5000
|
const streams = 5000
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -234,7 +234,7 @@ func rootHandler(serverName string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
var body string
|
var body string
|
||||||
rawBody, err := ioutil.ReadAll(r.Body)
|
rawBody, err := io.ReadAll(r.Body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
body = string(rawBody)
|
body = string(rawBody)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,6 +32,7 @@ const (
|
||||||
ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout"
|
ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout"
|
||||||
HTTPHostHeaderFlag = "http-host-header"
|
HTTPHostHeaderFlag = "http-host-header"
|
||||||
OriginServerNameFlag = "origin-server-name"
|
OriginServerNameFlag = "origin-server-name"
|
||||||
|
MatchSNIToHostFlag = "match-sni-to-host"
|
||||||
NoTLSVerifyFlag = "no-tls-verify"
|
NoTLSVerifyFlag = "no-tls-verify"
|
||||||
NoChunkedEncodingFlag = "no-chunked-encoding"
|
NoChunkedEncodingFlag = "no-chunked-encoding"
|
||||||
ProxyAddressFlag = "proxy-address"
|
ProxyAddressFlag = "proxy-address"
|
||||||
|
@ -44,14 +45,12 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type WarpRoutingConfig struct {
|
type WarpRoutingConfig struct {
|
||||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
||||||
ConnectTimeout config.CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
|
ConnectTimeout config.CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
|
||||||
TCPKeepAlive config.CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
|
TCPKeepAlive config.CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWarpRoutingConfig(raw *config.WarpRoutingConfig) WarpRoutingConfig {
|
func NewWarpRoutingConfig(raw *config.WarpRoutingConfig) WarpRoutingConfig {
|
||||||
cfg := WarpRoutingConfig{
|
cfg := WarpRoutingConfig{
|
||||||
Enabled: raw.Enabled,
|
|
||||||
ConnectTimeout: defaultWarpRoutingConnectTimeout,
|
ConnectTimeout: defaultWarpRoutingConnectTimeout,
|
||||||
TCPKeepAlive: defaultTCPKeepAlive,
|
TCPKeepAlive: defaultTCPKeepAlive,
|
||||||
}
|
}
|
||||||
|
@ -65,9 +64,7 @@ func NewWarpRoutingConfig(raw *config.WarpRoutingConfig) WarpRoutingConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WarpRoutingConfig) RawConfig() config.WarpRoutingConfig {
|
func (c *WarpRoutingConfig) RawConfig() config.WarpRoutingConfig {
|
||||||
raw := config.WarpRoutingConfig{
|
raw := config.WarpRoutingConfig{}
|
||||||
Enabled: c.Enabled,
|
|
||||||
}
|
|
||||||
if c.ConnectTimeout.Duration != defaultWarpRoutingConnectTimeout.Duration {
|
if c.ConnectTimeout.Duration != defaultWarpRoutingConnectTimeout.Duration {
|
||||||
raw.ConnectTimeout = &c.ConnectTimeout
|
raw.ConnectTimeout = &c.ConnectTimeout
|
||||||
}
|
}
|
||||||
|
@ -122,6 +119,7 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig {
|
||||||
var keepAliveTimeout = defaultKeepAliveTimeout
|
var keepAliveTimeout = defaultKeepAliveTimeout
|
||||||
var httpHostHeader string
|
var httpHostHeader string
|
||||||
var originServerName string
|
var originServerName string
|
||||||
|
var matchSNItoHost bool
|
||||||
var caPool string
|
var caPool string
|
||||||
var noTLSVerify bool
|
var noTLSVerify bool
|
||||||
var disableChunkedEncoding bool
|
var disableChunkedEncoding bool
|
||||||
|
@ -154,6 +152,9 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig {
|
||||||
if flag := OriginServerNameFlag; c.IsSet(flag) {
|
if flag := OriginServerNameFlag; c.IsSet(flag) {
|
||||||
originServerName = c.String(flag)
|
originServerName = c.String(flag)
|
||||||
}
|
}
|
||||||
|
if flag := MatchSNIToHostFlag; c.IsSet(flag) {
|
||||||
|
matchSNItoHost = c.Bool(flag)
|
||||||
|
}
|
||||||
if flag := tlsconfig.OriginCAPoolFlag; c.IsSet(flag) {
|
if flag := tlsconfig.OriginCAPoolFlag; c.IsSet(flag) {
|
||||||
caPool = c.String(flag)
|
caPool = c.String(flag)
|
||||||
}
|
}
|
||||||
|
@ -189,6 +190,7 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig {
|
||||||
KeepAliveTimeout: keepAliveTimeout,
|
KeepAliveTimeout: keepAliveTimeout,
|
||||||
HTTPHostHeader: httpHostHeader,
|
HTTPHostHeader: httpHostHeader,
|
||||||
OriginServerName: originServerName,
|
OriginServerName: originServerName,
|
||||||
|
MatchSNIToHost: matchSNItoHost,
|
||||||
CAPool: caPool,
|
CAPool: caPool,
|
||||||
NoTLSVerify: noTLSVerify,
|
NoTLSVerify: noTLSVerify,
|
||||||
DisableChunkedEncoding: disableChunkedEncoding,
|
DisableChunkedEncoding: disableChunkedEncoding,
|
||||||
|
@ -233,6 +235,9 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
|
||||||
if c.OriginServerName != nil {
|
if c.OriginServerName != nil {
|
||||||
out.OriginServerName = *c.OriginServerName
|
out.OriginServerName = *c.OriginServerName
|
||||||
}
|
}
|
||||||
|
if c.MatchSNIToHost != nil {
|
||||||
|
out.MatchSNIToHost = *c.MatchSNIToHost
|
||||||
|
}
|
||||||
if c.CAPool != nil {
|
if c.CAPool != nil {
|
||||||
out.CAPool = *c.CAPool
|
out.CAPool = *c.CAPool
|
||||||
}
|
}
|
||||||
|
@ -291,6 +296,8 @@ type OriginRequestConfig struct {
|
||||||
HTTPHostHeader string `yaml:"httpHostHeader" json:"httpHostHeader"`
|
HTTPHostHeader string `yaml:"httpHostHeader" json:"httpHostHeader"`
|
||||||
// Hostname on the origin server certificate.
|
// Hostname on the origin server certificate.
|
||||||
OriginServerName string `yaml:"originServerName" json:"originServerName"`
|
OriginServerName string `yaml:"originServerName" json:"originServerName"`
|
||||||
|
// Auto configure the Hostname on the origin server certificate.
|
||||||
|
MatchSNIToHost bool `yaml:"matchSNItoHost" json:"matchSNItoHost"`
|
||||||
// Path to the CA for the certificate of your origin.
|
// Path to the CA for the certificate of your origin.
|
||||||
// This option should be used only if your certificate is not signed by Cloudflare.
|
// This option should be used only if your certificate is not signed by Cloudflare.
|
||||||
CAPool string `yaml:"caPool" json:"caPool"`
|
CAPool string `yaml:"caPool" json:"caPool"`
|
||||||
|
@ -366,6 +373,12 @@ func (defaults *OriginRequestConfig) setOriginServerName(overrides config.Origin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (defaults *OriginRequestConfig) setMatchSNIToHost(overrides config.OriginRequestConfig) {
|
||||||
|
if val := overrides.MatchSNIToHost; val != nil {
|
||||||
|
defaults.MatchSNIToHost = *val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (defaults *OriginRequestConfig) setCAPool(overrides config.OriginRequestConfig) {
|
func (defaults *OriginRequestConfig) setCAPool(overrides config.OriginRequestConfig) {
|
||||||
if val := overrides.CAPool; val != nil {
|
if val := overrides.CAPool; val != nil {
|
||||||
defaults.CAPool = *val
|
defaults.CAPool = *val
|
||||||
|
@ -451,6 +464,7 @@ func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfi
|
||||||
cfg.setTCPKeepAlive(overrides)
|
cfg.setTCPKeepAlive(overrides)
|
||||||
cfg.setHTTPHostHeader(overrides)
|
cfg.setHTTPHostHeader(overrides)
|
||||||
cfg.setOriginServerName(overrides)
|
cfg.setOriginServerName(overrides)
|
||||||
|
cfg.setMatchSNIToHost(overrides)
|
||||||
cfg.setCAPool(overrides)
|
cfg.setCAPool(overrides)
|
||||||
cfg.setNoTLSVerify(overrides)
|
cfg.setNoTLSVerify(overrides)
|
||||||
cfg.setDisableChunkedEncoding(overrides)
|
cfg.setDisableChunkedEncoding(overrides)
|
||||||
|
@ -505,6 +519,7 @@ func ConvertToRawOriginConfig(c OriginRequestConfig) config.OriginRequestConfig
|
||||||
KeepAliveTimeout: keepAliveTimeout,
|
KeepAliveTimeout: keepAliveTimeout,
|
||||||
HTTPHostHeader: emptyStringToNil(c.HTTPHostHeader),
|
HTTPHostHeader: emptyStringToNil(c.HTTPHostHeader),
|
||||||
OriginServerName: emptyStringToNil(c.OriginServerName),
|
OriginServerName: emptyStringToNil(c.OriginServerName),
|
||||||
|
MatchSNIToHost: defaultBoolToNil(c.MatchSNIToHost),
|
||||||
CAPool: emptyStringToNil(c.CAPool),
|
CAPool: emptyStringToNil(c.CAPool),
|
||||||
NoTLSVerify: defaultBoolToNil(c.NoTLSVerify),
|
NoTLSVerify: defaultBoolToNil(c.NoTLSVerify),
|
||||||
DisableChunkedEncoding: defaultBoolToNil(c.DisableChunkedEncoding),
|
DisableChunkedEncoding: defaultBoolToNil(c.DisableChunkedEncoding),
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ingress
|
||||||
|
|
||||||
|
import "github.com/cloudflare/cloudflared/logger"
|
||||||
|
|
||||||
|
var (
|
||||||
|
TestLogger = logger.Create(nil)
|
||||||
|
)
|
|
@ -131,7 +131,7 @@ func newICMPProxy(listenIP netip.Addr, zone string, logger *zerolog.Logger, idle
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *packetResponder) error {
|
func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *packetResponder) error {
|
||||||
ctx, span := responder.requestSpan(ctx, pk)
|
_, span := responder.requestSpan(ctx, pk)
|
||||||
defer responder.exportSpan()
|
defer responder.exportSpan()
|
||||||
|
|
||||||
originalEcho, err := getICMPEcho(pk.Message)
|
originalEcho, err := getICMPEcho(pk.Message)
|
||||||
|
@ -139,10 +139,8 @@ func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *pa
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
span.SetAttributes(
|
observeICMPRequest(ip.logger, span, pk.Src.String(), pk.Dst.String(), originalEcho.ID, originalEcho.Seq)
|
||||||
attribute.Int("originalEchoID", originalEcho.ID),
|
|
||||||
attribute.Int("seq", originalEcho.Seq),
|
|
||||||
)
|
|
||||||
echoIDTrackerKey := flow3Tuple{
|
echoIDTrackerKey := flow3Tuple{
|
||||||
srcIP: pk.Src,
|
srcIP: pk.Src,
|
||||||
dstIP: pk.Dst,
|
dstIP: pk.Dst,
|
||||||
|
@ -189,6 +187,7 @@ func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *pa
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = icmpFlow.sendToDst(pk.Dst, pk.Message)
|
err = icmpFlow.sendToDst(pk.Dst, pk.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
|
@ -269,15 +268,12 @@ func (ip *icmpProxy) sendReply(ctx context.Context, reply *echoReply) error {
|
||||||
_, span := icmpFlow.responder.replySpan(ctx, ip.logger)
|
_, span := icmpFlow.responder.replySpan(ctx, ip.logger)
|
||||||
defer icmpFlow.responder.exportSpan()
|
defer icmpFlow.responder.exportSpan()
|
||||||
|
|
||||||
span.SetAttributes(
|
|
||||||
attribute.String("dst", reply.from.String()),
|
|
||||||
attribute.Int("echoID", reply.echo.ID),
|
|
||||||
attribute.Int("seq", reply.echo.Seq),
|
|
||||||
attribute.Int("originalEchoID", icmpFlow.originalEchoID),
|
|
||||||
)
|
|
||||||
if err := icmpFlow.returnToSrc(reply); err != nil {
|
if err := icmpFlow.returnToSrc(reply); err != nil {
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
observeICMPReply(ip.logger, span, reply.from.String(), reply.echo.ID, reply.echo.Seq)
|
||||||
|
span.SetAttributes(attribute.Int("originalEchoID", icmpFlow.originalEchoID))
|
||||||
tracing.End(span)
|
tracing.End(span)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,19 +78,19 @@ func checkInPingGroup() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
groupID := os.Getgid()
|
groupID := uint64(os.Getegid())
|
||||||
// Example content: 999 59999
|
// Example content: 999 59999
|
||||||
found := findGroupIDRegex.FindAll(file, 2)
|
found := findGroupIDRegex.FindAll(file, 2)
|
||||||
if len(found) == 2 {
|
if len(found) == 2 {
|
||||||
groupMin, err := strconv.ParseInt(string(found[0]), 10, 32)
|
groupMin, err := strconv.ParseUint(string(found[0]), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to determine minimum ping group ID")
|
return errors.Wrapf(err, "failed to determine minimum ping group ID")
|
||||||
}
|
}
|
||||||
groupMax, err := strconv.ParseInt(string(found[1]), 10, 32)
|
groupMax, err := strconv.ParseUint(string(found[1]), 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to determine minimum ping group ID")
|
return errors.Wrapf(err, "failed to determine maximum ping group ID")
|
||||||
}
|
}
|
||||||
if groupID < int(groupMin) || groupID > int(groupMax) {
|
if groupID < groupMin || groupID > groupMax {
|
||||||
return fmt.Errorf("Group ID %d is not between ping group %d to %d", groupID, groupMin, groupMax)
|
return fmt.Errorf("Group ID %d is not between ping group %d to %d", groupID, groupMin, groupMax)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -107,10 +107,7 @@ func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *pa
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
span.SetAttributes(
|
observeICMPRequest(ip.logger, span, pk.Src.String(), pk.Dst.String(), originalEcho.ID, originalEcho.Seq)
|
||||||
attribute.Int("originalEchoID", originalEcho.ID),
|
|
||||||
attribute.Int("seq", originalEcho.Seq),
|
|
||||||
)
|
|
||||||
|
|
||||||
shouldReplaceFunnelFunc := createShouldReplaceFunnelFunc(ip.logger, responder.datagramMuxer, pk, originalEcho.ID)
|
shouldReplaceFunnelFunc := createShouldReplaceFunnelFunc(ip.logger, responder.datagramMuxer, pk, originalEcho.ID)
|
||||||
newFunnelFunc := func() (packet.Funnel, error) {
|
newFunnelFunc := func() (packet.Funnel, error) {
|
||||||
|
@ -156,14 +153,8 @@ func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *pa
|
||||||
Int("originalEchoID", originalEcho.ID).
|
Int("originalEchoID", originalEcho.ID).
|
||||||
Msg("New flow")
|
Msg("New flow")
|
||||||
go func() {
|
go func() {
|
||||||
defer ip.srcFunnelTracker.Unregister(funnelID, icmpFlow)
|
ip.listenResponse(ctx, icmpFlow)
|
||||||
if err := ip.listenResponse(ctx, icmpFlow); err != nil {
|
ip.srcFunnelTracker.Unregister(funnelID, icmpFlow)
|
||||||
ip.logger.Debug().Err(err).
|
|
||||||
Str("src", pk.Src.String()).
|
|
||||||
Str("dst", pk.Dst.String()).
|
|
||||||
Int("originalEchoID", originalEcho.ID).
|
|
||||||
Msg("Failed to listen for ICMP echo response")
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
if err := icmpFlow.sendToDst(pk.Dst, pk.Message); err != nil {
|
if err := icmpFlow.sendToDst(pk.Dst, pk.Message); err != nil {
|
||||||
|
@ -179,17 +170,17 @@ func (ip *icmpProxy) Serve(ctx context.Context) error {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip *icmpProxy) listenResponse(ctx context.Context, flow *icmpEchoFlow) error {
|
func (ip *icmpProxy) listenResponse(ctx context.Context, flow *icmpEchoFlow) {
|
||||||
buf := make([]byte, mtu)
|
buf := make([]byte, mtu)
|
||||||
for {
|
for {
|
||||||
retryable, err := ip.handleResponse(ctx, flow, buf)
|
if done := ip.handleResponse(ctx, flow, buf); done {
|
||||||
if err != nil && !retryable {
|
return
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ip *icmpProxy) handleResponse(ctx context.Context, flow *icmpEchoFlow, buf []byte) (retryableErr bool, err error) {
|
// Listens for ICMP response and handles error logging
|
||||||
|
func (ip *icmpProxy) handleResponse(ctx context.Context, flow *icmpEchoFlow, buf []byte) (done bool) {
|
||||||
_, span := flow.responder.replySpan(ctx, ip.logger)
|
_, span := flow.responder.replySpan(ctx, ip.logger)
|
||||||
defer flow.responder.exportSpan()
|
defer flow.responder.exportSpan()
|
||||||
|
|
||||||
|
@ -199,33 +190,36 @@ func (ip *icmpProxy) handleResponse(ctx context.Context, flow *icmpEchoFlow, buf
|
||||||
|
|
||||||
n, from, err := flow.originConn.ReadFrom(buf)
|
n, from, err := flow.originConn.ReadFrom(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if flow.IsClosed() {
|
||||||
|
tracing.EndWithErrorStatus(span, fmt.Errorf("flow was closed"))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ip.logger.Error().Err(err).Str("socket", flow.originConn.LocalAddr().String()).Msg("Failed to read from ICMP socket")
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
return false, err
|
return true
|
||||||
}
|
}
|
||||||
reply, err := parseReply(from, buf[:n])
|
reply, err := parseReply(from, buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply")
|
ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to parse ICMP reply")
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
return true, err
|
return false
|
||||||
}
|
}
|
||||||
if !isEchoReply(reply.msg) {
|
if !isEchoReply(reply.msg) {
|
||||||
err := fmt.Errorf("Expect ICMP echo reply, got %s", reply.msg.Type)
|
err := fmt.Errorf("Expect ICMP echo reply, got %s", reply.msg.Type)
|
||||||
ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type)
|
ip.logger.Debug().Str("dst", from.String()).Msgf("Drop ICMP %s from reply", reply.msg.Type)
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
return true, err
|
return false
|
||||||
}
|
}
|
||||||
span.SetAttributes(
|
|
||||||
attribute.String("dst", reply.from.String()),
|
|
||||||
attribute.Int("echoID", reply.echo.ID),
|
|
||||||
attribute.Int("seq", reply.echo.Seq),
|
|
||||||
)
|
|
||||||
if err := flow.returnToSrc(reply); err != nil {
|
if err := flow.returnToSrc(reply); err != nil {
|
||||||
ip.logger.Debug().Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply")
|
ip.logger.Error().Err(err).Str("dst", from.String()).Msg("Failed to send ICMP reply")
|
||||||
tracing.EndWithErrorStatus(span, err)
|
tracing.EndWithErrorStatus(span, err)
|
||||||
return true, err
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeICMPReply(ip.logger, span, from.String(), reply.echo.ID, reply.echo.Seq)
|
||||||
tracing.End(span)
|
tracing.End(span)
|
||||||
return true, nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only linux uses flow3Tuple as FunnelID
|
// Only linux uses flow3Tuple as FunnelID
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -46,6 +47,7 @@ type flow3Tuple struct {
|
||||||
type icmpEchoFlow struct {
|
type icmpEchoFlow struct {
|
||||||
*packet.ActivityTracker
|
*packet.ActivityTracker
|
||||||
closeCallback func() error
|
closeCallback func() error
|
||||||
|
closed *atomic.Bool
|
||||||
src netip.Addr
|
src netip.Addr
|
||||||
originConn *icmp.PacketConn
|
originConn *icmp.PacketConn
|
||||||
responder *packetResponder
|
responder *packetResponder
|
||||||
|
@ -59,6 +61,7 @@ func newICMPEchoFlow(src netip.Addr, closeCallback func() error, originConn *icm
|
||||||
return &icmpEchoFlow{
|
return &icmpEchoFlow{
|
||||||
ActivityTracker: packet.NewActivityTracker(),
|
ActivityTracker: packet.NewActivityTracker(),
|
||||||
closeCallback: closeCallback,
|
closeCallback: closeCallback,
|
||||||
|
closed: &atomic.Bool{},
|
||||||
src: src,
|
src: src,
|
||||||
originConn: originConn,
|
originConn: originConn,
|
||||||
responder: responder,
|
responder: responder,
|
||||||
|
@ -86,9 +89,14 @@ func (ief *icmpEchoFlow) Equal(other packet.Funnel) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ief *icmpEchoFlow) Close() error {
|
func (ief *icmpEchoFlow) Close() error {
|
||||||
|
ief.closed.Store(true)
|
||||||
return ief.closeCallback()
|
return ief.closeCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ief *icmpEchoFlow) IsClosed() bool {
|
||||||
|
return ief.closed.Load()
|
||||||
|
}
|
||||||
|
|
||||||
// sendToDst rewrites the echo ID to the one assigned to this flow
|
// sendToDst rewrites the echo ID to the one assigned to this flow
|
||||||
func (ief *icmpEchoFlow) sendToDst(dst netip.Addr, msg *icmp.Message) error {
|
func (ief *icmpEchoFlow) sendToDst(dst netip.Addr, msg *icmp.Message) error {
|
||||||
ief.UpdateLastActive()
|
ief.UpdateLastActive()
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fortytw2/leaktest"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -18,6 +19,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFunnelIdleTimeout(t *testing.T) {
|
func TestFunnelIdleTimeout(t *testing.T) {
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
idleTimeout = time.Second
|
idleTimeout = time.Second
|
||||||
echoID = 42573
|
echoID = 42573
|
||||||
|
@ -73,13 +76,16 @@ func TestFunnelIdleTimeout(t *testing.T) {
|
||||||
require.NoError(t, proxy.Request(ctx, &pk, &newResponder))
|
require.NoError(t, proxy.Request(ctx, &pk, &newResponder))
|
||||||
validateEchoFlow(t, <-newMuxer.cfdToEdge, &pk)
|
validateEchoFlow(t, <-newMuxer.cfdToEdge, &pk)
|
||||||
|
|
||||||
|
time.Sleep(idleTimeout * 2)
|
||||||
cancel()
|
cancel()
|
||||||
<-proxyDone
|
<-proxyDone
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReuseFunnel(t *testing.T) {
|
func TestReuseFunnel(t *testing.T) {
|
||||||
|
defer leaktest.Check(t)()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
idleTimeout = time.Second
|
idleTimeout = time.Millisecond * 100
|
||||||
echoID = 42573
|
echoID = 42573
|
||||||
startSeq = 8129
|
startSeq = 8129
|
||||||
)
|
)
|
||||||
|
@ -135,6 +141,8 @@ func TestReuseFunnel(t *testing.T) {
|
||||||
require.True(t, found)
|
require.True(t, found)
|
||||||
require.Equal(t, funnel1, funnel2)
|
require.Equal(t, funnel1, funnel2)
|
||||||
|
|
||||||
|
time.Sleep(idleTimeout * 2)
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
<-proxyDone
|
<-proxyDone
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,10 +281,7 @@ func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *pa
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
requestSpan.SetAttributes(
|
observeICMPRequest(ip.logger, requestSpan, pk.Src.String(), pk.Dst.String(), echo.ID, echo.Seq)
|
||||||
attribute.Int("originalEchoID", echo.ID),
|
|
||||||
attribute.Int("seq", echo.Seq),
|
|
||||||
)
|
|
||||||
|
|
||||||
resp, err := ip.icmpEchoRoundtrip(pk.Dst, echo)
|
resp, err := ip.icmpEchoRoundtrip(pk.Dst, echo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -296,17 +293,17 @@ func (ip *icmpProxy) Request(ctx context.Context, pk *packet.ICMP, responder *pa
|
||||||
responder.exportSpan()
|
responder.exportSpan()
|
||||||
|
|
||||||
_, replySpan := responder.replySpan(ctx, ip.logger)
|
_, replySpan := responder.replySpan(ctx, ip.logger)
|
||||||
replySpan.SetAttributes(
|
|
||||||
attribute.Int("originalEchoID", echo.ID),
|
|
||||||
attribute.Int("seq", echo.Seq),
|
|
||||||
attribute.Int64("rtt", int64(resp.rtt())),
|
|
||||||
attribute.String("status", resp.status().String()),
|
|
||||||
)
|
|
||||||
err = ip.handleEchoReply(pk, echo, resp, responder)
|
err = ip.handleEchoReply(pk, echo, resp, responder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ip.logger.Err(err).Msg("Failed to send ICMP reply")
|
||||||
tracing.EndWithErrorStatus(replySpan, err)
|
tracing.EndWithErrorStatus(replySpan, err)
|
||||||
return errors.Wrap(err, "failed to handle ICMP echo reply")
|
return errors.Wrap(err, "failed to handle ICMP echo reply")
|
||||||
}
|
}
|
||||||
|
observeICMPReply(ip.logger, replySpan, pk.Dst.String(), echo.ID, echo.Seq)
|
||||||
|
replySpan.SetAttributes(
|
||||||
|
attribute.Int64("rtt", int64(resp.rtt())),
|
||||||
|
attribute.String("status", resp.status().String()),
|
||||||
|
)
|
||||||
tracing.End(replySpan)
|
tracing.End(replySpan)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
hostname = host
|
hostname = host
|
||||||
}
|
}
|
||||||
for i, rule := range ing.LocalRules {
|
for i, rule := range ing.InternalRules {
|
||||||
if rule.Matches(hostname, path) {
|
if rule.Matches(hostname, path) {
|
||||||
// Local rule matches return a negative rule index to distiguish local rules from user-defined rules in logs
|
// Local rule matches return a negative rule index to distiguish local rules from user-defined rules in logs
|
||||||
// Full range would be [-1 .. )
|
// Full range would be [-1 .. )
|
||||||
|
@ -77,7 +77,7 @@ func matchHost(ruleHost, reqHost string) bool {
|
||||||
// Ingress maps eyeball requests to origins.
|
// Ingress maps eyeball requests to origins.
|
||||||
type Ingress struct {
|
type Ingress struct {
|
||||||
// Set of ingress rules that are not added to remote config, e.g. management
|
// Set of ingress rules that are not added to remote config, e.g. management
|
||||||
LocalRules []Rule
|
InternalRules []Rule
|
||||||
// Rules that are provided by the user from remote or local configuration
|
// Rules that are provided by the user from remote or local configuration
|
||||||
Rules []Rule `json:"ingress"`
|
Rules []Rule `json:"ingress"`
|
||||||
Defaults OriginRequestConfig `json:"originRequest"`
|
Defaults OriginRequestConfig `json:"originRequest"`
|
||||||
|
@ -85,7 +85,7 @@ type Ingress struct {
|
||||||
|
|
||||||
// ParseIngress parses ingress rules, but does not send HTTP requests to the origins.
|
// ParseIngress parses ingress rules, but does not send HTTP requests to the origins.
|
||||||
func ParseIngress(conf *config.Configuration) (Ingress, error) {
|
func ParseIngress(conf *config.Configuration) (Ingress, error) {
|
||||||
if len(conf.Ingress) == 0 {
|
if conf == nil || len(conf.Ingress) == 0 {
|
||||||
return Ingress{}, ErrNoIngressRules
|
return Ingress{}, ErrNoIngressRules
|
||||||
}
|
}
|
||||||
return validateIngress(conf.Ingress, originRequestFromConfig(conf.OriginRequest))
|
return validateIngress(conf.Ingress, originRequestFromConfig(conf.OriginRequest))
|
||||||
|
@ -110,16 +110,18 @@ func ParseIngressFromConfigAndCLI(conf *config.Configuration, c *cli.Context, lo
|
||||||
// --bastion for ssh bastion service
|
// --bastion for ssh bastion service
|
||||||
ingressRules, err = parseCLIIngress(c, false)
|
ingressRules, err = parseCLIIngress(c, false)
|
||||||
if errors.Is(err, ErrNoIngressRulesCLI) {
|
if errors.Is(err, ErrNoIngressRulesCLI) {
|
||||||
// Only log a warning if the tunnel is not a remotely managed tunnel and the config
|
// If no token is provided, the probability of NOT being a remotely managed tunnel is higher.
|
||||||
// will be loaded after connecting.
|
// So, we should warn the user that no ingress rules were found, because remote configuration will most likely not exist.
|
||||||
if !c.IsSet("token") {
|
if !c.IsSet("token") {
|
||||||
log.Warn().Msgf(ErrNoIngressRulesCLI.Error())
|
log.Warn().Msgf(ErrNoIngressRulesCLI.Error())
|
||||||
}
|
}
|
||||||
return newDefaultOrigin(c, log), nil
|
return newDefaultOrigin(c, log), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Ingress{}, err
|
return Ingress{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ingressRules, nil
|
return ingressRules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,14 +150,10 @@ func parseCLIIngress(c *cli.Context, allowURLFromArgs bool) (Ingress, error) {
|
||||||
// newDefaultOrigin always returns a 503 response code to help indicate that there are no ingress
|
// newDefaultOrigin always returns a 503 response code to help indicate that there are no ingress
|
||||||
// rules setup, but the tunnel is reachable.
|
// rules setup, but the tunnel is reachable.
|
||||||
func newDefaultOrigin(c *cli.Context, log *zerolog.Logger) Ingress {
|
func newDefaultOrigin(c *cli.Context, log *zerolog.Logger) Ingress {
|
||||||
noRulesService := newDefaultStatusCode(log)
|
defaultRule := GetDefaultIngressRules(log)
|
||||||
defaults := originRequestFromSingleRule(c)
|
defaults := originRequestFromSingleRule(c)
|
||||||
ingress := Ingress{
|
ingress := Ingress{
|
||||||
Rules: []Rule{
|
Rules: defaultRule,
|
||||||
{
|
|
||||||
Service: &noRulesService,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Defaults: defaults,
|
Defaults: defaults,
|
||||||
}
|
}
|
||||||
return ingress
|
return ingress
|
||||||
|
@ -219,6 +217,17 @@ func (ing Ingress) CatchAll() *Rule {
|
||||||
return &ing.Rules[len(ing.Rules)-1]
|
return &ing.Rules[len(ing.Rules)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets the default ingress rule that will be return 503 status
|
||||||
|
// code for all incoming requests.
|
||||||
|
func GetDefaultIngressRules(log *zerolog.Logger) []Rule {
|
||||||
|
noRulesService := newDefaultStatusCode(log)
|
||||||
|
return []Rule{
|
||||||
|
{
|
||||||
|
Service: &noRulesService,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validateAccessConfiguration(cfg *config.AccessConfig) error {
|
func validateAccessConfiguration(cfg *config.AccessConfig) error {
|
||||||
if !cfg.Required {
|
if !cfg.Required {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -43,6 +43,11 @@ ingress:
|
||||||
require.Equal(t, "https", s.scheme)
|
require.Equal(t, "https", s.scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseIngressNilConfig(t *testing.T) {
|
||||||
|
_, err := ParseIngress(nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseIngress(t *testing.T) {
|
func TestParseIngress(t *testing.T) {
|
||||||
localhost8000 := MustParseURL(t, "https://localhost:8000")
|
localhost8000 := MustParseURL(t, "https://localhost:8000")
|
||||||
localhost8001 := MustParseURL(t, "https://localhost:8001")
|
localhost8001 := MustParseURL(t, "https://localhost:8001")
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
"github.com/go-jose/go-jose/v4/jwt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
issuer = fmt.Sprintf(cloudflareAccessCertsURL, "testteam")
|
||||||
|
)
|
||||||
|
|
||||||
|
type accessTokenClaims struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
jwt.Claims
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJWTValidator(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||||
|
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
issued := time.Now()
|
||||||
|
claims := accessTokenClaims{
|
||||||
|
Email: "test@example.com",
|
||||||
|
Type: "app",
|
||||||
|
Claims: jwt.Claims{
|
||||||
|
Issuer: issuer,
|
||||||
|
Subject: "ee239b7a-e3e6-4173-972a-8fbe9d99c04f",
|
||||||
|
Audience: []string{""},
|
||||||
|
Expiry: jwt.NewNumericDate(issued.Add(time.Hour)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(issued),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
token := signToken(t, claims, key)
|
||||||
|
req.Header.Add(headerKeyAccessJWTAssertion, token)
|
||||||
|
|
||||||
|
keySet := oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{key.Public()}}
|
||||||
|
config := &oidc.Config{
|
||||||
|
SkipClientIDCheck: true,
|
||||||
|
SupportedSigningAlgs: []string{string(jose.ES256)},
|
||||||
|
}
|
||||||
|
verifier := oidc.NewVerifier(issuer, &keySet, config)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
audTags []string
|
||||||
|
aud jwt.Audience
|
||||||
|
error bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
audTags: []string{
|
||||||
|
"0bc545634b1732494b3f9472794a549c883fabd48de9dfe0e0413e59c3f96c38",
|
||||||
|
"d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba",
|
||||||
|
},
|
||||||
|
aud: jwt.Audience{"d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba"},
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid no match",
|
||||||
|
audTags: []string{
|
||||||
|
"0bc545634b1732494b3f9472794a549c883fabd48de9dfe0e0413e59c3f96c38",
|
||||||
|
"d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba",
|
||||||
|
},
|
||||||
|
aud: jwt.Audience{"09dc377143841843ecca28b196bdb1ec1675af38c8b7b60c7def5876c8877157"},
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid empty check",
|
||||||
|
audTags: []string{},
|
||||||
|
aud: jwt.Audience{"09dc377143841843ecca28b196bdb1ec1675af38c8b7b60c7def5876c8877157"},
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid absent aud",
|
||||||
|
audTags: []string{
|
||||||
|
"0bc545634b1732494b3f9472794a549c883fabd48de9dfe0e0413e59c3f96c38",
|
||||||
|
"d7ec5b7fda23ffa8f8c8559fb37c66a2278208a78dbe376a3394b5ffec6911ba",
|
||||||
|
},
|
||||||
|
aud: jwt.Audience{""},
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
validator := JWTValidator{
|
||||||
|
IDTokenVerifier: verifier,
|
||||||
|
audTags: test.audTags,
|
||||||
|
}
|
||||||
|
claims.Audience = test.aud
|
||||||
|
token := signToken(t, claims, key)
|
||||||
|
req.Header.Set(headerKeyAccessJWTAssertion, token)
|
||||||
|
|
||||||
|
result, err := validator.Handle(context.Background(), req)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.error, result.ShouldFilterRequest)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signToken(t *testing.T, token accessTokenClaims, key *ecdsa.PrivateKey) string {
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, &jose.SignerOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
payload, err := json.Marshal(token)
|
||||||
|
require.NoError(t, err)
|
||||||
|
jws, err := signer.Sign(payload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
jwt, err := jws.CompactSerialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
return jwt
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
|
@ -31,15 +32,32 @@ func DefaultStreamHandler(originConn io.ReadWriter, remoteConn net.Conn, log *ze
|
||||||
|
|
||||||
// tcpConnection is an OriginConnection that directly streams to raw TCP.
|
// tcpConnection is an OriginConnection that directly streams to raw TCP.
|
||||||
type tcpConnection struct {
|
type tcpConnection struct {
|
||||||
conn net.Conn
|
net.Conn
|
||||||
|
writeTimeout time.Duration
|
||||||
|
logger *zerolog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *tcpConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
func (tc *tcpConnection) Stream(_ context.Context, tunnelConn io.ReadWriter, _ *zerolog.Logger) {
|
||||||
stream.Pipe(tunnelConn, tc.conn, log)
|
stream.Pipe(tunnelConn, tc, tc.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *tcpConnection) Write(b []byte) (int, error) {
|
||||||
|
if tc.writeTimeout > 0 {
|
||||||
|
if err := tc.Conn.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
|
||||||
|
tc.logger.Err(err).Msg("Error setting write deadline for TCP connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nBytes, err := tc.Conn.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
tc.logger.Err(err).Msg("Error writing to the TCP connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nBytes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *tcpConnection) Close() {
|
func (tc *tcpConnection) Close() {
|
||||||
tc.conn.Close()
|
tc.Conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// tcpOverWSConnection is an OriginConnection that streams to TCP over WS.
|
// tcpOverWSConnection is an OriginConnection that streams to TCP over WS.
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -20,7 +19,6 @@ import (
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/logger"
|
|
||||||
"github.com/cloudflare/cloudflared/socks"
|
"github.com/cloudflare/cloudflared/socks"
|
||||||
"github.com/cloudflare/cloudflared/stream"
|
"github.com/cloudflare/cloudflared/stream"
|
||||||
"github.com/cloudflare/cloudflared/websocket"
|
"github.com/cloudflare/cloudflared/websocket"
|
||||||
|
@ -32,7 +30,6 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testLogger = logger.Create(nil)
|
|
||||||
testMessage = []byte("TestStreamOriginConnection")
|
testMessage = []byte("TestStreamOriginConnection")
|
||||||
testResponse = []byte(fmt.Sprintf("echo-%s", testMessage))
|
testResponse = []byte(fmt.Sprintf("echo-%s", testMessage))
|
||||||
)
|
)
|
||||||
|
@ -40,7 +37,8 @@ var (
|
||||||
func TestStreamTCPConnection(t *testing.T) {
|
func TestStreamTCPConnection(t *testing.T) {
|
||||||
cfdConn, originConn := net.Pipe()
|
cfdConn, originConn := net.Pipe()
|
||||||
tcpConn := tcpConnection{
|
tcpConn := tcpConnection{
|
||||||
conn: cfdConn,
|
Conn: cfdConn,
|
||||||
|
writeTimeout: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
eyeballConn, edgeConn := net.Pipe()
|
eyeballConn, edgeConn := net.Pipe()
|
||||||
|
@ -67,7 +65,7 @@ func TestStreamTCPConnection(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
tcpConn.Stream(ctx, edgeConn, testLogger)
|
tcpConn.Stream(ctx, edgeConn, TestLogger)
|
||||||
require.NoError(t, errGroup.Wait())
|
require.NoError(t, errGroup.Wait())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +92,7 @@ func TestDefaultStreamWSOverTCPConnection(t *testing.T) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
tcpOverWSConn.Stream(ctx, edgeConn, testLogger)
|
tcpOverWSConn.Stream(ctx, edgeConn, TestLogger)
|
||||||
require.NoError(t, errGroup.Wait())
|
require.NoError(t, errGroup.Wait())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +116,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, status := range statusCodes {
|
for _, status := range statusCodes {
|
||||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []byte(sendMessage), body)
|
require.Equal(t, []byte(sendMessage), body)
|
||||||
|
|
||||||
|
@ -148,7 +146,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||||
|
|
||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
tcpOverWSConn.Stream(ctx, edgeConn, testLogger)
|
tcpOverWSConn.Stream(ctx, edgeConn, TestLogger)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -160,7 +158,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer wsForwarderInConn.Close()
|
defer wsForwarderInConn.Close()
|
||||||
|
|
||||||
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, testLogger)
|
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, TestLogger)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -180,7 +178,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, status, resp.StatusCode)
|
assert.Equal(t, status, resp.StatusCode)
|
||||||
require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName))
|
require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName))
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []byte(echoMessage), body)
|
require.Equal(t, []byte(echoMessage), body)
|
||||||
|
|
||||||
|
@ -210,7 +208,7 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
|
||||||
originConn.Close()
|
originConn.Close()
|
||||||
}()
|
}()
|
||||||
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
|
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
|
||||||
tcpOverWSConn.Stream(ctx, eyeballConn, testLogger)
|
tcpOverWSConn.Stream(ctx, eyeballConn, TestLogger)
|
||||||
})
|
})
|
||||||
server := httptest.NewServer(handler)
|
server := httptest.NewServer(handler)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
"golang.org/x/net/icmp"
|
"golang.org/x/net/icmp"
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
|
@ -15,9 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// funnelIdleTimeout controls how long to wait to close a funnel without send/return
|
mtu = 1500
|
||||||
funnelIdleTimeout = time.Second * 10
|
|
||||||
mtu = 1500
|
|
||||||
// icmpRequestTimeoutMs controls how long to wait for a reply
|
// icmpRequestTimeoutMs controls how long to wait for a reply
|
||||||
icmpRequestTimeoutMs = 1000
|
icmpRequestTimeoutMs = 1000
|
||||||
)
|
)
|
||||||
|
@ -32,8 +32,9 @@ type icmpRouter struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewICMPRouter doesn't return an error if either ipv4 proxy or ipv6 proxy can be created. The machine might only
|
// NewICMPRouter doesn't return an error if either ipv4 proxy or ipv6 proxy can be created. The machine might only
|
||||||
// support one of them
|
// support one of them.
|
||||||
func NewICMPRouter(ipv4Addr, ipv6Addr netip.Addr, ipv6Zone string, logger *zerolog.Logger) (*icmpRouter, error) {
|
// funnelIdleTimeout controls how long to wait to close a funnel without send/return
|
||||||
|
func NewICMPRouter(ipv4Addr, ipv6Addr netip.Addr, ipv6Zone string, logger *zerolog.Logger, funnelIdleTimeout time.Duration) (*icmpRouter, error) {
|
||||||
ipv4Proxy, ipv4Err := newICMPProxy(ipv4Addr, "", logger, funnelIdleTimeout)
|
ipv4Proxy, ipv4Err := newICMPProxy(ipv4Addr, "", logger, funnelIdleTimeout)
|
||||||
ipv6Proxy, ipv6Err := newICMPProxy(ipv6Addr, ipv6Zone, logger, funnelIdleTimeout)
|
ipv6Proxy, ipv6Err := newICMPProxy(ipv6Addr, ipv6Zone, logger, funnelIdleTimeout)
|
||||||
if ipv4Err != nil && ipv6Err != nil {
|
if ipv4Err != nil && ipv6Err != nil {
|
||||||
|
@ -102,3 +103,25 @@ func getICMPEcho(msg *icmp.Message) (*icmp.Echo, error) {
|
||||||
func isEchoReply(msg *icmp.Message) bool {
|
func isEchoReply(msg *icmp.Message) bool {
|
||||||
return msg.Type == ipv4.ICMPTypeEchoReply || msg.Type == ipv6.ICMPTypeEchoReply
|
return msg.Type == ipv4.ICMPTypeEchoReply || msg.Type == ipv6.ICMPTypeEchoReply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func observeICMPRequest(logger *zerolog.Logger, span trace.Span, src string, dst string, echoID int, seq int) {
|
||||||
|
logger.Debug().
|
||||||
|
Str("src", src).
|
||||||
|
Str("dst", dst).
|
||||||
|
Int("originalEchoID", echoID).
|
||||||
|
Int("originalEchoSeq", seq).
|
||||||
|
Msg("Received ICMP request")
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.Int("originalEchoID", echoID),
|
||||||
|
attribute.Int("seq", seq),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func observeICMPReply(logger *zerolog.Logger, span trace.Span, dst string, echoID int, seq int) {
|
||||||
|
logger.Debug().Str("dst", dst).Int("echoID", echoID).Int("seq", seq).Msg("Sent ICMP reply to edge")
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("dst", dst),
|
||||||
|
attribute.Int("echoID", echoID),
|
||||||
|
attribute.Int("seq", seq),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue