Compare commits

...

36 Commits

Author SHA1 Message Date
Hrushikesh Deshpande f72b69340b ci: add Semgrep OSS scanning workflow 2026-04-23 20:51:46 -04:00
Miguel da Costa Martins Marcelino 9f084e6800 TUN-10386: Add Table Renderer
The goals of this PR are twofold:

## **1. Introduce a new renderer to output to `stdout`**

Implement the table renderer that will be used to report the results to stdout. The renderer should output something similar to this:

```
─── CONNECTIVITY PRE-CHECKS ──────────────────────────────────────────────────
COMPONENT            TARGET                         STATUS   DETAILS
DNS Resolution       region1.v2.argotunnel.com      PASS     Resolved successfully
DNS Resolution       region2.v2.argotunnel.com      PASS     Resolved successfully
UDP Connectivity     Port 7844 (QUIC)               PASS     Handshake successful
TCP Connectivity     Port 7844 (HTTP/2)             PASS     TLS handshake successful
Cloudflare API       api.cloudflare.com:443         PASS     Reachable

SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.
──────────────────────────────────────────────────────────────────────────────
```

## **2. Add a log-level renderer**

Add support for structured logging to print the table results as logs. Below is an example of how logs should look like:

```
{"level":"info","run_id":"52828729-dfwd-45b3-w12f-727cbdb4cbd4", "component":"DNS Resolution","target":"region1.v2.argotunnel.com","status":"pass","details":"Resolved successfully","time":"2024-01-15T10:30:00Z","message":"precheck"}
{"level":"info","run_id":"52828729-dfwd-45b3-w12f-727cbdb4cbd4", "component":"DNS Resolution","target":"region2.v2.argotunnel.com","status":"pass","details":"Resolved successfully","time":"2024-01-15T10:30:00Z","message":"precheck"}
{"level":"info","run_id":"52828729-dfwd-45b3-w12f-727cbdb4cbd4", "component":"QUIC Connectivity","target":"Port 7844 (QUIC)","status":"pass","details":"Handshake successful","time":"2024-01-15T10:30:01Z","message":"precheck"}
{"level":"info","run_id":"52828729-dfwd-45b3-w12f-727cbdb4cbd4", "component":"HTTP/2 Connectivity","target":"Port 7844 (HTTP/2)","status":"pass","details":"TLS handshake successful","time":"2024-01-15T10:30:01Z","message":"precheck"}
{"level":"info","run_id":"52828729-dfwd-45b3-w12f-727cbdb4cbd4", "component":"Management API","target":"api.cloudflare.com:443","status":"pass","details":"Reachable","time":"2024-01-15T10:30:01Z","message":"precheck"}
{"level":"info","run_id":"52828729-dfwd-45b3-w12f-727cbdb4cbd4", "hard_fail":false,"suggested_protocol":"quic","time":"2024-01-15T10:30:01Z","message":"precheck complete"}
```
2026-04-23 19:04:06 +00:00
Miguel da Costa Martins Marcelino df54d27710 TUN-10385: Add connectivity checks foundation
This adds the foundation for the new cloudflared pre-checks by creating a new prechecks package. This adds the following:

* types.go: Status, CheckResult, Report, Config (add IPVersion allregions.ConfigIPVersion field to Config)
* interfaces.go: DNSResolver, TCPDialer, QUICDialer, ManagementDialer
2026-04-15 22:40:23 +00:00
Miguel da Costa Martins Marcelino b0b898c235 TUN-10383: Set edge-ip-version to auto
To allow pre-checks to test both IPv6 and IPv4, we must change the default value of edge-ip-version's from 4 to auto. This will allows the tunnel (and pre-check) to probe both IPv4 and IPv6 addresses by default, respecting the system's DNS preference. Instead of always preferring IPv4, cloudflared will now use whichever address family the system resolver returns first.
2026-04-14 16:11:59 +00:00
Miguel da Costa Martins Marcelino 5287a9e24b TUN-10384: Probe TLS Helper
Add `ProbeTLSSettings` helper to connection/protocol.go that returns new settings with the `probe.cftunnel.com` SNI for pre-checks.
2026-04-14 15:35:03 +00:00
Miguel da Costa Martins Marcelino e2a71cbecc chore: Fix errors in cmd
Trying to fix the following errors that showed up in CI, which became an issue when doing the pre-check work in https://gitlab.cfdata.org/cloudflare/tun/cloudflared/-/merge_requests/1814:

```
cmd/cloudflared/tunnel/cmd.go:454:29: Error return value of `metricsListener.Close` is not checked (errcheck)
	defer metricsListener.Close()
	                           ^
cmd/cloudflared/tunnel/cmd.go:573:18: Error return value of `file.Close` is not checked (errcheck)
	defer file.Close()
	                ^
cmd/cloudflared/tunnel/cmd.go:574:13: Error return value of `fmt.Fprintf` is not checked (errcheck)
	fmt.Fprintf(file, "%d", os.Getpid())
	           ^
cmd/cloudflared/tunnel/cmd.go:47:2: G101: Potential hardcoded credentials: Password in URL (gosec)
	sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
	^
cmd/cloudflared/tunnel/cmd.go:348:23: G703: Path traversal via taint analysis (gosec)
			if err := os.Rename(tmpTraceFile.Name(), traceOutputFilepath); err != nil {
			                   ^
cmd/cloudflared/tunnel/cmd.go:354:21: G703: Path traversal via taint analysis (gosec)
				err := os.Remove(tmpTraceFile.Name())
				                ^
cmd/cloudflared/tunnel/cmd.go:568:15: G304: Potential file inclusion via variable (gosec)
	file, err := os.Create(expandedPath)
	             ^
cmd/cloudflared/tunnel/cmd.go:260:10: ST1005: error strings should not be capitalized (staticcheck)
		return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
		       ^
cmd/cloudflared/tunnel/cmd.go:1146:5: SA4011: ineffective break statement. Did you mean to break out of the outer loop? (staticcheck)
				break
				^
9 issues:
* errcheck: 3
* gosec: 4
* staticcheck: 2
```
2026-04-14 14:56:10 +00:00
Harshini Ramanujam a0e55fc969 SECENG-13056 update gokeyless install instructions on pkg.cloudflare.com/index.html
Updating the instructions - now that I have updated gokeyless packages to sign with both keys. Will check in with TUN team to release.

Closes SECENG-13056
2026-04-10 08:59:48 -04:00
GoncaloGarcia 1e9deb1002 TUN-9952: Bump go to 1.26 2026-04-06 13:04:18 +01:00
GoncaloGarcia d2a87e9b93 Release 2026.3.0 2026-03-06 12:53:40 +00:00
João "Pisco" Fernandes c0bc3bdbf0 fix: Update go-sentry and go-oidc to address CVE's 2026-03-05 19:10:16 +00:00
João "Pisco" Fernandes 29b3a7aa7e chore: Addressing small fixes and typos 2026-03-05 16:53:48 +00:00
Gonçalo Garcia 372a4b7079 TUN-10292: Add cloudflared management token command
Create new management token command to support different resource
permissions (logs, admin, host_details). This fixes failing component
tests that need admin-level tokens to access management endpoints.

- Add ManagementResource enum values: Admin, HostDetails
- Create cmd/cloudflared/management package with token command
- Extract shared utilities to cliutil/management.go (GetManagementToken, CreateStderrLogger)
- Refactor tail/cmd.go to use shared utilities
- Update component tests to use new command with admin resource

Closes TUN-10292
2026-03-05 16:31:24 +00:00
Luis Neto 649705d291 TUN-10258: add agents.md
* chore: add agents.md

this was generated by opencode's /init command
2026-02-24 11:17:27 +00:00
Luis Neto 839b874cad TUN-10267: Update mods to fix CVE GO-2026-4394
* TUN-10267: Update mods to fix CVE GO-2026-4394

Closes TUN-10267
2026-02-23 14:22:02 +00:00
Gonçalo Garcia 059f4d9898 TUN-10247: Update tail command to use /management/logs endpoint
* TUN-10247: Update tail command to use /management/logs endpoint

The /management endpoint will be deprecated in favor of new /management/resource endpoints. Because of that, we'll need cloudflared to use the new endpoint.

Closes TUN-10247
2026-02-20 15:40:25 +00:00
João "Pisco" Fernandes a0bcbf6a44 TUN-9858: Add more information to proxy-dns removal message
## Summary
Add link to deprecation announcement and alternative DNS-over-HTTPS client recommendation in the proxy-dns error message.
2026-02-11 17:59:38 +00:00
João "Pisco" Fernandes 66587173e2 Release 2026.2.0 2026-02-06 14:21:32 +00:00
João "Pisco" Fernandes 9388e7f48c TUN-9858: Remove proxy-dns feature from cloudflared
Remove the DNS over HTTPS (DoH) proxy feature built on CoreDNS due to
security vulnerabilities (GO-2025-3942, GO-2026-4289).

This removes:
- Standalone proxy-dns command (cloudflared proxy-dns)
- Tunnel subcommand (cloudflared tunnel proxy-dns)
- Proxy-dns flags for tunnel run (--proxy-dns, --proxy-dns-port, etc.)
- Config file resolver section support
- tunneldns/ package (CoreDNS-based implementation)
- Related component tests

BREAKING CHANGE: The proxy-dns feature is no longer available.
Users should migrate to alternative DNS over HTTPS solutions.
2026-02-06 12:43:53 +00:00
Luis Neto d6cb78aeb4 TUN-10216: TUN fix cloudflare vulnerabilities GO-2026-4340 and GO-2026-4341
* TUN-10216: TUN fix cloudflare vulnerabilities GO-2026-4340 and GO-2026-4341

Closes TUN-10216
2026-02-06 10:01:07 +00:00
João "Pisco" Fernandes d7c62aed71 Release 2026.1.2 2026-01-23 12:45:53 +00:00
João "Pisco" Fernandes 2b95c61044 Revert "TUN-9863: Update pipelines to use cloudflared EV Certificate"
This reverts commit 789a9b110d.
2026-01-23 12:45:36 +00:00
João "Pisco" Fernandes efd0189121 Revert "TUN-9886 notarize cloudflared"
This reverts commit 9abcfece66.
2026-01-21 13:33:53 +00:00
Andi Anderson 9abcfece66 TUN-9886 notarize cloudflared 2026-01-21 12:14:06 +00:00
João "Pisco" Fernandes 8aebc38b2f Release 2026.1.1 2026-01-19 18:11:47 +00:00
João "Pisco" Fernandes b4f675c082 fix: rpm bundling and rpm key import 2026-01-19 18:10:47 +00:00
João "Pisco" Fernandes a29afd842e fix: Fix wixl bundling tool for windows msi packages 2026-01-19 12:48:30 +00:00
João "Pisco" Fernandes d9cdd49eec fix: Update boto3 to run on trixie 2026-01-19 12:47:30 +00:00
João "Pisco" Fernandes 8af01d583c Release 2026.1.0 2026-01-16 17:48:15 +00:00
chungthuang 3e6d8ed216 TUN-9998: Don't need to read origin cert to determine if the endpoint is fedramp 2026-01-14 15:07:51 +00:00
chungthuang 1e907e99b5 TUN-9910: Make the metadata key to carry HTTP status over QUIC transport a constant 2026-01-14 14:28:08 +00:00
João "Pisco" Fernandes 18eab5879f TUN-10162: Update go to 1.24.11 and Debian distroless to debian13 2026-01-13 13:08:16 +00:00
GitLab Maintenance Bot 0d2a7a0385 Replace jira.cfops.it with jira.cfdata.org in connection/http2_test.go 2025-11-21 05:33:09 +00:00
João "Pisco" Fernandes 789a9b110d TUN-9863: Update pipelines to use cloudflared EV Certificate 2025-11-19 18:05:33 +00:00
João "Pisco" Fernandes 31f45fb505 TUN-9800: Migrate apt internal builds to Gitlab 2025-11-10 14:43:10 +00:00
João "Pisco" Fernandes 17533b124c Release 2025.11.1 2025-11-07 16:30:58 +00:00
João "Pisco" Fernandes 9ce16c5aac TUN-9800: Fix docker hub push step 2025-11-07 15:26:22 +00:00
584 changed files with 50502 additions and 63642 deletions
+151
View File
@@ -0,0 +1,151 @@
.register_inputs: &register_inputs
stage: release-internal
runOnBranches: "^master$"
COMPONENT: "common"
.register_inputs_stable_bookworm: &register_inputs_stable_bookworm
<<: *register_inputs
runOnChangesTo: ['RELEASE_NOTES']
FLAVOR: "bookworm"
SERIES: "stable"
.register_inputs_stable_trixie: &register_inputs_stable_trixie
<<: *register_inputs
runOnChangesTo: ['RELEASE_NOTES']
FLAVOR: "trixie"
SERIES: "stable"
.register_inputs_next_bookworm: &register_inputs_next_bookworm
<<: *register_inputs
FLAVOR: "bookworm"
SERIES: next
.register_inputs_next_trixie: &register_inputs_next_trixie
<<: *register_inputs
FLAVOR: "trixie"
SERIES: next
################################################
### Generate Debian Package for Internal APT ###
################################################
.cloudflared-apt-build: &cloudflared_apt_build
stage: package
needs:
- ci-image-get-image-ref
- linux-packaging # For consistency, we only run this job after we knew we could build the packages for external delivery
image: $BUILD_IMAGE
cache: {}
script:
- make cloudflared-deb
artifacts:
paths:
- cloudflared*.deb
##############
### Stable ###
##############
cloudflared-amd64-stable:
<<: *cloudflared_apt_build
rules:
- !reference [.default-rules, run-on-release]
variables: &amd64-stable-vars
GOOS: linux
GOARCH: amd64
FIPS: true
ORIGINAL_NAME: true
CGO_ENABLED: 1
cloudflared-arm64-stable:
<<: *cloudflared_apt_build
rules:
- !reference [.default-rules, run-on-release]
variables: &arm64-stable-vars
GOOS: linux
GOARCH: arm64
FIPS: false # TUN-7595
ORIGINAL_NAME: true
CGO_ENABLED: 1
############
### Next ###
############
cloudflared-amd64-next:
<<: *cloudflared_apt_build
rules:
- !reference [.default-rules, run-on-master]
variables:
<<: *amd64-stable-vars
NIGHTLY: true
cloudflared-arm64-next:
<<: *cloudflared_apt_build
rules:
- !reference [.default-rules, run-on-master]
variables:
<<: *arm64-stable-vars
NIGHTLY: true
include:
- local: .ci/commons.gitlab-ci.yml
##########################################
### Publish Packages to Internal Repos ###
##########################################
# Bookworm AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_bookworm
jobPrefix: cloudflared-bookworm-amd64
needs: &amd64-stable ["cloudflared-amd64-stable"]
# Bookworm ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_bookworm
jobPrefix: cloudflared-bookworm-arm64
needs: &arm64-stable ["cloudflared-arm64-stable"]
# Trixie AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_trixie
jobPrefix: cloudflared-trixie-amd64
needs: *amd64-stable
# Trixie ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_trixie
jobPrefix: cloudflared-trixie-arm64
needs: *arm64-stable
##################################################
### Publish Nightly Packages to Internal Repos ###
##################################################
# Bookworm AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_bookworm
jobPrefix: cloudflared-nightly-bookworm-amd64
needs: &amd64-next ['cloudflared-amd64-next']
# Bookworm ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_bookworm
jobPrefix: cloudflared-nightly-bookworm-arm64
needs: &arm64-next ['cloudflared-arm64-next']
# Trixie AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_trixie
jobPrefix: cloudflared-nightly-trixie-amd64
needs: *amd64-next
# Trixie ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_trixie
jobPrefix: cloudflared-nightly-trixie-arm64
needs: *arm64-next
+7 -15
View File
@@ -20,21 +20,13 @@
- if: $CI_COMMIT_BRANCH != null && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: on_success
- when: never
# This before_script is injected into every job that runs on master meaning that if there is no tag the step
# will succeed but only write "No tag present - Skipping" to the console.
.check-tag:
before_script:
- |
# Check if there is a Git tag pointing to HEAD
echo "Tag found: $(git tag --points-at HEAD | grep .)"
if git tag --points-at HEAD | grep .; then
echo "Tag found: $(git tag --points-at HEAD | grep .)"
export "VERSION=$(git tag --points-at HEAD | grep .)"
else
echo "No tag present — skipping."
exit 0
fi
# Rules to run the job only when a release happens
run-on-release:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
changes:
- 'RELEASE_NOTES'
when: on_success
- when: never
.component-tests:
image: $BUILD_IMAGE
+34 -27
View File
@@ -1,32 +1,39 @@
ARG CLOUDFLARE_DOCKER_REGISTRY_HOST
FROM ${CLOUDFLARE_DOCKER_REGISTRY_HOST:-registry.cfdata.org}/stash/cf/debian-images/bookworm/main:2025.7.0@sha256:6350da2f7e728dae2c1420f6dafc38e23cacc0b399d3d5b2f40fe48d9c8ff1ca
FROM ${CLOUDFLARE_DOCKER_REGISTRY_HOST:-registry.cfdata.org}/stash/cf/debian-images/trixie/main:2026.1.0@sha256:e32092fd01520f5ae7de1fa6bb5a721720900ebeaa48e98f36f6f86168833cd7
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install --no-install-recommends --allow-downgrades -y \
build-essential \
git \
go-boring=1.24.9-1 \
libffi-dev \
procps \
python3-dev \
python3-pip \
python3-setuptools \
python3-venv \
# libmsi and libgcab are libraries the wixl binary depends on.
libmsi-dev \
libgcab-dev \
# deb and rpm build tools
rubygem-fpm \
rpm \
# create deb and rpm repository files
reprepro \
createrepo-c && \
rm -rf /var/lib/apt/lists/* && \
# Install wixl
curl -o /usr/local/bin/wixl -L https://pkg.cloudflare.com/binaries/wixl && \
chmod a+x /usr/local/bin/wixl && \
mkdir -p opt
apt-get upgrade -y && \
apt-get install --no-install-recommends --allow-downgrades -y \
build-essential \
git \
go-boring=1.26.0-1 \
libffi-dev \
procps \
python3-dev \
python3-pip \
python3-setuptools \
python3-venv \
# tool to create msi packages
wixl \
# install ruby and rpm which are required to install fpm package builder
rpm \
ruby \
ruby-dev \
rubygems \
# create deb and rpm repository files
reprepro \
createrepo-c \
# gcc for cross architecture compilation in arm
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross && \
rm -rf /var/lib/apt/lists/* && \
# Install fpm gem
gem install fpm --no-document && \
# Initialize rpm repository, SQL Lite DB
mkdir -p /var/lib/rpm && \
rpm --initdb && \
chmod -R 777 /var/lib/rpm && \
# Create work directory
mkdir -p opt
WORKDIR /opt
+4 -4
View File
@@ -1,11 +1,11 @@
.golang-inputs: &golang_inputs
runOnMR: true
runOnBranches: '^master$'
runOnBranches: "^master$"
outputDir: artifacts
runner: linux-x86-8cpu-16gb
stage: build
golangVersion: "boring-1.24"
imageVersion: "3371-f5539bd6f83d@sha256:a2a68f580070f9411d0d3155959ed63b700ef319b5fcc62db340e92227bbc628"
golangVersion: "boring-1.26"
imageVersion: "3501-fc698419a625@sha256:aff18c895a50e8451982484e629319e893ebd411675e0482d247079801253e7b"
CGO_ENABLED: 1
.default-packaging-job: &packaging-job-defaults
@@ -65,7 +65,7 @@ include:
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
inputs:
<<: *golang_inputs
runOnBranches: '^$'
runOnBranches: "^$"
stage: validate
jobPrefix: vulncheck
GOLANG_MAKE_TARGET: vulncheck
+1 -1
View File
@@ -28,7 +28,7 @@ macos-build-cloudflared: &mac-build
- '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64'
- ARCH=$(uname -m)
- echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH
- ./.ci/scripts/mac/install-go.sh
- ./.ci/scripts/mac/install-go.sh "$MAC_GO_VERSION"
- BUILD_SCRIPT=.ci/scripts/mac/build.sh
- if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi
- set -euo pipefail
+15 -6
View File
@@ -16,15 +16,18 @@ include:
- release-cloudflared-to-r2
commentImageRefs: false
runner: vm-linux-x86-4cpu-8gb
DOCKER_USER_BRANCH: svcgithubdockerhubcloudflar045
DOCKER_PASSWORD_BRANCH: gitlab/cloudflare/tun/cloudflared/_dev/dockerhub/svc_password/data
# Based on if the CI reference is protected or not the CI component will
# either use _BRANCH or _PROD, therefore, to prevent the pipelines from failing
# we simply set both to the same value.
DOCKER_USER_BRANCH: &docker-hub-user svcgithubdockerhubcloudflar045
DOCKER_PASSWORD_BRANCH: &docker-hub-password gitlab/cloudflare/tun/cloudflared/_dev/dockerhub/svc_password/data
DOCKER_USER_PROD: *docker-hub-user
DOCKER_PASSWORD_PROD: *docker-hub-password
EXTRA_DIB_ARGS: --overwrite
.default-release-job: &release-job-defaults
stage: release
image: $BUILD_IMAGE
rules:
- !reference [.default-rules, run-on-master]
cache:
paths:
- .cache/pip
@@ -71,7 +74,8 @@ include:
###########################################
release-cloudflared-to-github:
<<: *release-job-defaults
extends: .check-tag
rules:
- !reference [.default-rules, run-on-release]
needs:
- ci-image-get-image-ref
- linux-packaging
@@ -86,7 +90,8 @@ release-cloudflared-to-github:
#########################################
release-cloudflared-to-r2:
<<: *release-job-defaults
extends: .check-tag
rules:
- !reference [.default-rules, run-on-release]
needs:
- ci-image-get-image-ref
- linux-packaging # We only release non-FIPS binaries to R2
@@ -99,6 +104,8 @@ release-cloudflared-to-r2:
#################################################
release-cloudflared-nightly-to-r2:
<<: *release-job-defaults
rules:
- !reference [.default-rules, run-on-master]
variables:
<<: *release-job-variables
R2_BUCKET: cloudflared-pkgs-next
@@ -115,6 +122,8 @@ release-cloudflared-nightly-to-r2:
#############################
generate-version-file:
<<: *release-job-defaults
rules:
- !reference [.default-rules, run-on-release]
needs:
- ci-image-get-image-ref
script:
+1 -1
View File
@@ -1,5 +1,5 @@
#!/bin/bash
set -e -o pipefail
set -e -u -o pipefail
# Fetch cloudflared from the artifacts folder
mv ./artifacts/cloudflared ./cloudflared
+1 -1
View File
@@ -1,5 +1,5 @@
#!/bin/bash
set -e -o pipefail
set -e -u -o pipefail
OUTPUT=$(go run -mod=readonly golang.org/x/tools/cmd/goimports@v0.30.0 -l -d -local github.com/cloudflare/cloudflared $(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc))
+1 -1
View File
@@ -1,5 +1,5 @@
#!/bin/bash
set -e -o pipefail
set -e -u -o pipefail
BRANCH="master"
TMP_PATH="$PWD/tmp"
+1
View File
@@ -1,4 +1,5 @@
#!/bin/bash
set -e -u -o pipefail
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
echo $VERSION
+1
View File
@@ -1,4 +1,5 @@
#!/bin/bash
set -e -u -o pipefail
# Check if architecture argument is provided
if [ $# -eq 0 ]; then
+6 -2
View File
@@ -2,9 +2,13 @@ rm -rf /tmp/go
export GOCACHE=/tmp/gocache
rm -rf $GOCACHE
brew install go@1.24
if [ -z "$1" ]
then
echo "No go version supplied"
fi
brew install "$1"
go version
which go
go env
+2
View File
@@ -1,4 +1,6 @@
#!/bin/bash
set -e -u -o pipefail
python3 -m venv env
. env/bin/activate
pip install pynacl==1.4.0 pygithub==1.55
+2 -2
View File
@@ -1,5 +1,5 @@
#!/bin/bash
set -e -o pipefail
set -e -u -o pipefail
# Check if a make target is provided as an argument
if [ $# -eq 0 ]; then
@@ -14,5 +14,5 @@ python3 -m venv venv
source venv/bin/activate
# Our release scripts are written in python, so we should install their dependecies here.
pip install pynacl==1.4.0 pygithub==1.55 boto3==1.22.9 python-gnupg==0.4.9
pip install pynacl==1.4.0 pygithub==1.55 boto3==1.42.30 python-gnupg==0.4.9
make $MAKE_TARGET
+17 -16
View File
@@ -1,16 +1,17 @@
#!/bin/bash
set -e
set -e -u
# Define the file to store the list of vulnerabilities to ignore.
IGNORE_FILE=".vulnignore"
go version
# Check if the ignored vulnerabilities file exists. If not, create an empty one.
if [ ! -f "$IGNORE_FILE" ]; then
touch "$IGNORE_FILE"
echo "Created an empty file to store ignored vulnerabilities: $IGNORE_FILE"
echo "# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line." >> "$IGNORE_FILE"
echo "# You can also add comments on the same line after the ID." >> "$IGNORE_FILE"
echo "" >> "$IGNORE_FILE"
touch "$IGNORE_FILE"
echo "Created an empty file to store ignored vulnerabilities: $IGNORE_FILE"
echo "# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line." >>"$IGNORE_FILE"
echo "# You can also add comments on the same line after the ID." >>"$IGNORE_FILE"
echo "" >>"$IGNORE_FILE"
fi
# Run govulncheck and capture its output.
@@ -31,22 +32,22 @@ echo "====================================="
CLEAN_IGNORES=$(grep -v '^\s*#' "$IGNORE_FILE" | cut -d'#' -f1 | sed 's/ //g' | sort -u || true)
# Filter out the ignored vulnerabilities.
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability')
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability' || true)
# If the list of ignored vulnerabilities is not empty, filter them out.
if [ -n "$CLEAN_IGNORES" ]; then
UNIGNORED_VULNS=$(echo "$UNIGNORED_VULNS" | grep -vFf <(echo "$CLEAN_IGNORES") || true)
UNIGNORED_VULNS=$(echo "$UNIGNORED_VULNS" | grep -vFf <(echo "$CLEAN_IGNORES") || true)
fi
# If there are any vulnerabilities that were not in our ignore list, print them and exit with an error.
if [ -n "$UNIGNORED_VULNS" ]; then
echo "🚨 Found new, unignored vulnerabilities:"
echo "-------------------------------------"
echo "$UNIGNORED_VULNS"
echo "-------------------------------------"
echo "Exiting with an error. ❌"
exit 1
echo "🚨 Found new, unignored vulnerabilities:"
echo "-------------------------------------"
echo "$UNIGNORED_VULNS"
echo "-------------------------------------"
echo "Exiting with an error. ❌"
exit 1
else
echo "🎉 No new vulnerabilities found. All clear! ✨"
exit 0
echo "🎉 No new vulnerabilities found. All clear! ✨"
exit 0
fi
+2 -2
View File
@@ -18,7 +18,7 @@ windows-build-cloudflared:
<<: *windows-build-defaults
stage: build
script:
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${GO_VERSION}" ".\.ci\scripts\windows\builds.ps1"
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\builds.ps1"
artifacts:
paths:
- artifacts/*
@@ -73,7 +73,7 @@ windows-component-tests-cloudflared:
script:
# We have to decode the secret we encoded on the `windows-load-env-variables` job
- $env:COMPONENT_TESTS_ORIGINCERT = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:COMPONENT_TESTS_ORIGINCERT))
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${GO_VERSION}" ".\.ci\scripts\windows\component-test.ps1"
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\component-test.ps1"
artifacts:
reports:
junit: report.xml
+23 -17
View File
@@ -1,24 +1,30 @@
name: Semgrep OSS scan
on:
pull_request: {}
push:
branches: [main, master]
workflow_dispatch: {}
push:
branches:
- main
- master
schedule:
- cron: '0 0 * * *'
name: Semgrep config
- cron: '0 0 25 * *'
concurrency:
group: semgrep-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-latest
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
SEMGREP_URL: https://cloudflare.semgrep.dev
SEMGREP_APP_URL: https://cloudflare.semgrep.dev
SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
container:
image: semgrep/semgrep
name: semgrep-oss
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v4
- run: semgrep ci
- uses: actions/checkout@v5
with:
fetch-depth: 1
- id: cache-semgrep
uses: actions/cache@v5
with:
path: ~/.local
key: semgrep-1.160.0-${{ runner.os }}
- if: steps.cache-semgrep.outputs.cache-hit != 'true'
run: pip install --user semgrep==1.160.0
- run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: semgrep scan --config=auto
+1
View File
@@ -18,3 +18,4 @@ ssh_server_tests/.env
/.cover
built_artifacts/
component-tests/.venv
/artifacts
+20 -4
View File
@@ -1,5 +1,7 @@
variables:
GO_VERSION: "go1.24.9"
GO_VERSION: "1.26.0"
MAC_GO_VERSION: "go@$GO_VERSION"
WIN_GO_VERSION: "go$GO_VERSION"
GIT_DEPTH: "0"
default:
@@ -7,7 +9,18 @@ default:
VAULT_ID_TOKEN:
aud: https://vault.cfdata.org
stages: [sync, pre-build, build, validate, test, package, release, review]
stages:
[
sync,
pre-build,
build,
validate,
test,
package,
release,
release-internal,
review,
]
include:
#####################################################
@@ -45,9 +58,12 @@ include:
#####################################################
- local: .ci/release.gitlab-ci.yml
#####################################################
########## Release Packages Internally ##############
#####################################################
- local: .ci/apt-internal.gitlab-ci.yml
#####################################################
############## Manual Claude Review #################
#####################################################
- component: $CI_SERVER_FQDN/cloudflare/ci/ai/review@~latest
inputs:
whenToRun: "manual"
+14 -8
View File
@@ -1,3 +1,5 @@
version: "2"
linters:
enable:
# Some of the linters below are commented out. We should uncomment and start running them, but they return
@@ -14,10 +16,7 @@ linters:
- errcheck # Errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases.
- errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error.
- exhaustive # Check exhaustiveness of enum switch statements.
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification.
- goimports # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode.
- gosec # Inspects source code for security problems.
- gosimple # Linter for Go source code that specializes in simplifying code.
- govet # Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes.
- ineffassign # Detects when assignments to existing variables are not used.
- importas # Enforces consistent import aliases.
@@ -36,7 +35,13 @@ linters:
- wastedassign # Finds wasted assignment statements.
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.
- zerologlint # Detects the wrong usage of zerolog that a user forgets to dispatch with Send or Msg.
# Other linters are disabled, list of all is here: https://golangci-lint.run/usage/linters/
# Other linters are disabled, list of all is here: https://golangci-lint.run/usage/linters/
formatters:
enable:
- gofmt # Formats code according to Go standard formatting
- goimports # Formats imports and groups them properly
run:
timeout: 5m
modules-download-mode: vendor
@@ -44,9 +49,10 @@ run:
# output configuration options
output:
formats:
- format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
text:
colors: true
print-linter-name: true
print-issued-lines: true
issues:
# Maximum issues count per one linter.
@@ -67,7 +73,7 @@ issues:
new: true
# Show only new issues created after git revision `REV`.
# Default: ""
new-from-rev: ac34f94d423273c8fa8fdbb5f2ac60e55f2c77d5
new-from-rev: d2a87e9b93456ad7f82417400f4209d513668487
# Show issues in any part of update files (requires new-from-rev or new-from-patch).
# Default: false
whole-files: true
-1
View File
@@ -1,3 +1,2 @@
# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line.
# You can also add comments on the same line after the ID.
GO-2025-3942 # Ignore core-dns vulnerability since we will be removing the proxy-dns feature in the near future
+251
View File
@@ -0,0 +1,251 @@
# Cloudflared
Cloudflare's command-line tool and networking daemon written in Go.
Production-grade tunneling and network connectivity services used by millions of
developers and organizations worldwide.
## Essential Commands
### Build & Test (Always run before commits)
```bash
# Full development check (run before any commit)
make test lint
# Build for current platform
make cloudflared
# Run all unit tests with coverage
make test
make cover
# Run specific test
go test -run TestFunctionName ./path/to/package
# Run tests with race detection
go test -race ./...
```
### Platform-Specific Builds
```bash
# Linux
TARGET_OS=linux TARGET_ARCH=amd64 make cloudflared
# Windows
TARGET_OS=windows TARGET_ARCH=amd64 make cloudflared
# macOS ARM64
TARGET_OS=darwin TARGET_ARCH=arm64 make cloudflared
# FIPS compliant build
FIPS=true make cloudflared
```
### Code Quality & Formatting
```bash
# Run linter (38+ enabled linters)
make lint
# Auto-fix formatting
make fmt
gofmt -w .
goimports -w .
# Security scanning
make vet
# Component tests (Python integration tests)
cd component-tests && python -m pytest test_file.py::test_function_name
```
## Project Knowledge
### Package Structure
- Use meaningful package names that reflect functionality
- Package names should be lowercase, single words when possible
- Avoid generic names like `util`, `common`, `helper`
### Function and Method Guidelines
```go
// Good: Clear purpose, proper error handling
func (c *Connection) HandleRequest(ctx context.Context, req *http.Request) error {
if req == nil {
return errors.New("request cannot be nil")
}
// Implementation...
return nil
}
```
### Error Handling
- Always handle errors explicitly, never ignore them
- Use `fmt.Errorf` for error wrapping
- Create meaningful error messages with context
- Use error variables for common errors
```go
// Good error handling patterns
if err != nil {
return fmt.Errorf("failed to process connection: %w", err)
}
```
### Logging Standards
- Use `github.com/rs/zerolog` for structured logging
- Include relevant context fields
- Use appropriate log levels (Debug, Info, Warn, Error)
```go
logger.Info().
Str("tunnelID", tunnel.ID).
Int("connIndex", connIndex).
Msg("Connection established")
```
### Testing Patterns
- Use `github.com/stretchr/testify` for assertions
- Test files end with `_test.go`
- Use table-driven tests for multiple scenarios
- Always use `t.Parallel()` for parallel-safe tests
- Use meaningful test names that describe behavior
```go
func TestMetricsListenerCreation(t *testing.T) {
t.Parallel()
// Test implementation
assert.Equal(t, expected, actual)
require.NoError(t, err)
}
```
### Constants and Variables
```go
const (
MaxGracePeriod = time.Minute * 3
MaxConcurrentStreams = math.MaxUint32
LogFieldConnIndex = "connIndex"
)
var (
// Group related variables
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
flushableContentTypes = []string{sseContentType, grpcContentType, sseJsonContentType}
)
```
### Type Definitions
- Define interfaces close to their usage
- Keep interfaces small and focused
- Use descriptive names for complex types
```go
type TunnelConnection interface {
Serve(ctx context.Context) error
}
type TunnelProperties struct {
Credentials Credentials
QuickTunnelUrl string
}
```
## Key Architectural Patterns
### Context Usage
- Always accept `context.Context` as first parameter for long-running operations
- Respect context cancellation in loops and blocking operations
- Pass context through call chains
### Concurrency
- Use channels for goroutine communication
- Protect shared state with mutexes
- Prefer `sync.RWMutex` for read-heavy workloads
### Configuration
- Use structured configuration with validation
- Support both file-based and CLI flag configuration
- Provide sensible defaults
### Metrics and Observability
- Instrument code with Prometheus metrics
- Use OpenTelemetry for distributed tracing
- Include structured logging with relevant context
## Boundaries
### ✅ Always Do
- Run `make test lint` before any commit
- Handle all errors explicitly with proper context
- Use `github.com/rs/zerolog` for all logging
- Add `t.Parallel()` to all parallel-safe tests
- Follow the import grouping conventions
- Use meaningful variable and function names
- Include context.Context for long-running operations
- Close resources in defer statements
### ⚠️ Ask First Before
- Adding new dependencies to go.mod
- Modifying CI/CD configuration files
- Changing build system or Makefile
- Modifying component test infrastructure
- Adding new linter rules or changing golangci-lint config
- Making breaking changes to public APIs
- Changing logging levels or structured logging fields
### 🚫 Never Do
- Ignore errors without explicit handling (`_ = err`)
- Use generic package names (`util`, `helper`, `common`)
- Commit code that fails `make test lint`
- Use `fmt.Print*` instead of structured logging
- Modify vendor dependencies directly
- Commit secrets, credentials, or sensitive data
- Use deprecated or unsafe Go patterns
- Skip testing for new functionality
- Remove existing tests unless they're genuinely invalid
## Dependencies Management
- Use Go modules (`go.mod`) exclusively
- Vendor dependencies for reproducible builds
- Keep dependencies up-to-date and secure
- Prefer standard library when possible
- Cloudflared uses a fork of quic-go always check release notes before bumping
this dependency.
## Security Considerations
- FIPS compliance support available
- Vulnerability scanning integrated in CI
- Credential handling follows security best practices
- Network security with TLS/QUIC protocols
- Regular security audits and updates
- Post quantum encryption
## Common Patterns to Follow
1. **Graceful shutdown**: Always implement proper cleanup
2. **Resource management**: Close resources in defer statements
3. **Error propagation**: Wrap errors with meaningful context
4. **Configuration validation**: Validate inputs early
5. **Logging consistency**: Use structured logging throughout
6. **Testing coverage**: Aim for comprehensive test coverage
7. **Documentation**: Comment exported functions and types
Remember: This is a mission-critical networking tool used in production by many
organizations. Code quality, security, and reliability are paramount.
+16 -1
View File
@@ -1,3 +1,18 @@
## 2026.4.0
### Breaking Change
- The default value of `--edge-ip-version` has changed from `4` to `auto`. This means cloudflared will now use whichever address family (IPv4 or IPv6) the system resolver returns first, instead of always preferring IPv4. Users who require IPv4-only connections should explicitly set `--edge-ip-version 4`.
## 2026.2.0
### Breaking Change
- Removes the `proxy-dns` feature from cloudflared. This feature allowed running a local DNS over HTTPS (DoH) proxy.
Users who relied on this functionality should migrate to alternative solutions.
Removed commands and flags:
- `cloudflared proxy-dns`
- `cloudflared tunnel proxy-dns`
- `--proxy-dns`, `--proxy-dns-port`, `--proxy-dns-address`, `--proxy-dns-upstream`, `--proxy-dns-max-upstream-conns`, `--proxy-dns-bootstrap`
- `resolver` section in configuration file
## 2025.7.1
### Notices
- `cloudflared` will no longer officially support Debian and Ubuntu distros that reached end-of-life: `buster`, `bullseye`, `impish`, `trusty`.
@@ -281,7 +296,7 @@ of uptime. Previous cloudflared versions will soon be unable to run legacy tempo
### Bug Fixes
- Tunnel create and delete commands no longer use path to credentials from the configuration file.
If you need ot place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
If you need to place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
- Access ssh-gen creates properly named keys for SSH short lived certs.
+2 -2
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.24.9 AS builder
FROM golang:1.26.0 AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
@@ -20,7 +20,7 @@ COPY . .
RUN make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian12:nonroot
FROM gcr.io/distroless/base-debian13:nonroot
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+2 -2
View File
@@ -1,5 +1,5 @@
# use a builder image for building cloudflare
FROM golang:1.24.9 AS builder
FROM golang:1.26.0 AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
@@ -15,7 +15,7 @@ COPY . .
RUN GOOS=linux GOARCH=amd64 make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian12:nonroot
FROM gcr.io/distroless/base-debian13:nonroot
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+2 -2
View File
@@ -1,5 +1,5 @@
# use a builder image for building cloudflare
FROM golang:1.24.9 AS builder
FROM golang:1.26.0 AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
@@ -15,7 +15,7 @@ COPY . .
RUN GOOS=linux GOARCH=arm64 make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian12:nonroot-arm64
FROM gcr.io/distroless/base-debian13:nonroot-arm64
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+3 -3
View File
@@ -10,7 +10,7 @@ You can also use `cloudflared` to access Tunnel origins (that are protected with
at Layer 4 (i.e., not HTTP/websocket), which is relevant for use cases such as SSH, RDP, etc.
Such usages are available under `cloudflared access help`.
You can instead use [WARP client](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/warp/)
You can instead use [WARP client](https://developers.cloudflare.com/warp-client/)
to access private origins behind Tunnels for Layer 4 traffic without requiring `cloudflared access` commands on the client side.
@@ -40,7 +40,7 @@ User documentation for Cloudflare Tunnel can be found at https://developers.clou
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels to serve traffic to your origins.
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/)
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/)
* Route traffic to that Tunnel:
* Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/dns/)
* Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/public-load-balancers/)
@@ -62,7 +62,7 @@ For example, as of January 2023 Cloudflare will support cloudflared version 2023
### Requirements
- [GNU Make](https://www.gnu.org/software/make/)
- [capnp](https://capnproto.org/install.html)
- [go >= 1.24](https://go.dev/doc/install)
- [go >= 1.26](https://go.dev/doc/install)
- Optional tools:
- [capnpc-go](https://pkg.go.dev/zombiezen.com/go/capnproto2/capnpc-go)
- [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)
+34
View File
@@ -1,3 +1,37 @@
2026.3.0
- 2026-03-05 TUN-10292: Add cloudflared management token command
- 2026-03-03 chore: Addressing small fixes and typos
- 2026-03-03 fix: Update go-sentry and go-oidc to address CVE's
- 2026-02-24 TUN-10258: add agents.md
- 2026-02-23 TUN-10267: Update mods to fix CVE GO-2026-4394
- 2026-02-20 TUN-10247: Update tail command to use /management/logs endpoint
- 2026-02-11 TUN-9858: Add more information to proxy-dns removal message
2026.2.0
- 2026-02-06 TUN-10216: TUN fix cloudflare vulnerabilities GO-2026-4340 and GO-2026-4341
- 2026-02-02 TUN-9858: Remove proxy-dns feature from cloudflared
2026.1.2
- 2026-01-23 Revert "TUN-9863: Update pipelines to use cloudflared EV Certificate"
- 2026-01-21 Revert "TUN-9886 notarize cloudflared"
- 2025-12-12 TUN-9886 notarize cloudflared
2026.1.1
- 2026-01-19 fix: Update boto3 to run on trixie
- 2026-01-19 fix: Fix wixl bundling tool for windows msi packages
- 2026-01-19 fix: rpm bundling and rpm key import
2026.1.0
- 2026-01-13 TUN-10162: Update go to 1.24.11 and Debian distroless to debian13
- 2025-11-21 Replace jira.cfops.it with jira.cfdata.org in connection/http2_test.go
- 2025-11-19 TUN-9863: Update pipelines to use cloudflared EV Certificate
- 2025-11-07 TUN-9800: Migrate apt internal builds to Gitlab
- 2025-11-04 TUN-9998: Don't need to read origin cert to determine if the endpoint is fedramp
- 2025-10-13 TUN-9910: Make the metadata key to carry HTTP status over QUIC transport a constant
2025.11.1
- 2025-11-07 TUN-9800: Fix docker hub push step
2025.11.0
- 2025-11-06 TUN-9863: Introduce Code Signing for Windows Builds
- 2025-11-06 TUN-9800: Prefix gitlab steps with operating system
+8 -12
View File
@@ -45,9 +45,7 @@ type baseEndpoints struct {
var _ Client = (*RESTClient)(nil)
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
baseURL = strings.TrimSuffix(baseURL, "/")
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
@@ -68,7 +66,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
}
http2.ConfigureTransport(&httpTransport)
_ = http2.ConfigureTransport(&httpTransport)
return &RESTClient{
baseEndpoints: &baseEndpoints{
accountLevel: *accountLevelEndpoint,
@@ -161,7 +159,6 @@ func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T
if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
break
}
}
return fullResponse, nil
}
@@ -179,14 +176,13 @@ func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*r
}
var parsedRspBody []*T
return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
}
return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
}
type response struct {
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Errors []apiError `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Pagination Pagination `json:"result_info,omitempty"`
@@ -206,19 +202,19 @@ func (r *response) checkErrors() error {
if len(r.Errors) == 1 {
return r.Errors[0]
}
var messages string
var messagesBuilder strings.Builder
for _, e := range r.Errors {
messages += fmt.Sprintf("%s; ", e)
messagesBuilder.WriteString(fmt.Sprintf("%s; ", e))
}
return fmt.Errorf("API errors: %s", messages)
return fmt.Errorf("API errors: %s", messagesBuilder.String())
}
type apiErr struct {
type apiError struct {
Code json.Number `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e apiErr) Error() string {
func (e apiError) Error() string {
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
}
+1 -1
View File
@@ -8,7 +8,7 @@ type TunnelClient interface {
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
GetTunnelToken(tunnelID uuid.UUID) (string, error)
GetManagementToken(tunnelID uuid.UUID) (string, error)
GetManagementToken(tunnelID uuid.UUID, resource ManagementResource) (string, error)
DeleteTunnel(tunnelID uuid.UUID, cascade bool) error
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
+29 -11
View File
@@ -15,6 +15,27 @@ import (
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
type ManagementResource int
const (
Logs ManagementResource = iota
Admin
HostDetails
)
func (r ManagementResource) String() string {
switch r {
case Logs:
return "logs"
case Admin:
return "admin"
case HostDetails:
return "host_details"
default:
return ""
}
}
type Tunnel struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
@@ -50,10 +71,6 @@ type newTunnel struct {
TunnelSecret []byte `json:"tunnel_secret"`
}
type managementRequest struct {
Resources []string `json:"resources"`
}
type CleanupParams struct {
queryParams url.Values
}
@@ -137,15 +154,16 @@ func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error
return "", r.statusCodeToError("get tunnel token", resp)
}
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) {
// managementEndpointPath returns the path segment for a management resource endpoint
func managementEndpointPath(tunnelID uuid.UUID, res ManagementResource) string {
return fmt.Sprintf("%v/management/%s", tunnelID, res.String())
}
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID, res ManagementResource) (token string, err error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID))
endpoint.Path = path.Join(endpoint.Path, managementEndpointPath(tunnelID, res))
body := &managementRequest{
Resources: []string{"logs"},
}
resp, err := r.sendRequest("POST", endpoint, body)
resp, err := r.sendRequest("POST", endpoint, nil)
if err != nil {
return "", errors.Wrap(err, "REST request failed")
}
+71 -7
View File
@@ -2,7 +2,6 @@ package cfapi
import (
"bytes"
"fmt"
"net"
"reflect"
"strings"
@@ -11,6 +10,7 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var loc, _ = time.LoadLocation("UTC")
@@ -52,7 +52,6 @@ func Test_unmarshalTunnel(t *testing.T) {
}
func TestUnmarshalTunnelOk(t *testing.T) {
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
expected := Tunnel{
ID: uuid.Nil,
@@ -61,12 +60,11 @@ func TestUnmarshalTunnelOk(t *testing.T) {
Connections: []Connection{},
}
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, &expected, actual)
require.NoError(t, err)
require.Equal(t, &expected, actual)
}
func TestUnmarshalTunnelErr(t *testing.T) {
tests := []string{
`abc`,
`{"success": true, "result": abc}`,
@@ -76,7 +74,73 @@ func TestUnmarshalTunnelErr(t *testing.T) {
for i, test := range tests {
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
assert.Error(t, err, "Test #%v failed", i)
}
}
func TestManagementResource_String(t *testing.T) {
tests := []struct {
name string
resource ManagementResource
want string
}{
{
name: "Logs",
resource: Logs,
want: "logs",
},
{
name: "Admin",
resource: Admin,
want: "admin",
},
{
name: "HostDetails",
resource: HostDetails,
want: "host_details",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.resource.String())
})
}
}
func TestManagementResource_String_Unknown(t *testing.T) {
unknown := ManagementResource(999)
assert.Equal(t, "", unknown.String())
}
func TestManagementEndpointPath(t *testing.T) {
tunnelID := uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292")
tests := []struct {
name string
resource ManagementResource
want string
}{
{
name: "Logs resource",
resource: Logs,
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/logs",
},
{
name: "Admin resource",
resource: Admin,
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/admin",
},
{
name: "HostDetails resource",
resource: HostDetails,
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/host_details",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := managementEndpointPath(tunnelID, tt.resource)
assert.Equal(t, tt.want, got)
})
}
}
@@ -97,6 +161,6 @@ func TestUnmarshalConnections(t *testing.T) {
}},
}
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, []*ActiveClient{&expected}, actual)
}
+2 -52
View File
@@ -1,52 +1,2 @@
pinned_go: &pinned_go go-boring=1.24.9-1
build_dir: &build_dir /cfsetup_build
default-flavor: bookworm
bookworm: &bookworm
build-fips-internal-deb:
build_dir: *build_dir
builddeps: &build_fips_deb_deps
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- export FIPS=true
- export ORIGINAL_NAME=true
- make cloudflared-deb
build-internal-deb-nightly-amd64:
build_dir: *build_dir
builddeps: *build_fips_deb_deps
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- export NIGHTLY=true
- export FIPS=true
- export ORIGINAL_NAME=true
- make cloudflared-deb
build-internal-deb-nightly-arm64:
build_dir: *build_dir
builddeps: *build_fips_deb_deps
post-cache:
- export GOOS=linux
- export GOARCH=arm64
- export NIGHTLY=true
# - export FIPS=true # TUN-7595
- export ORIGINAL_NAME=true
- make cloudflared-deb
build-deb-arm64:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
post-cache:
- export GOOS=linux
- export GOARCH=arm64
- make cloudflared-deb
trixie: *bookworm
# A valid cfsetup.yaml is required but we dont have any real config to specify
dummy_key: true
+20
View File
@@ -3,6 +3,7 @@ package access
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
@@ -23,6 +24,24 @@ func parseRequestHeaders(values []string) http.Header {
return headers
}
// bracketBareIPv6 wraps bare IPv6 addresses in a URL with square brackets.
// Go 1.26 tightened net/url parsing to strictly require RFC 3986 bracket syntax
// for IPv6 addresses in URLs. Before Go 1.26, bare forms like "http://::1" were
// accepted; now they are rejected. This function detects bare IPv6 in the host
// portion and brackets it so that url.ParseRequestURI can parse it correctly.
func bracketBareIPv6(input string) string {
prefix := input[:strings.Index(input, "://")+3]
rest := input[len(prefix):]
host := rest
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
host = rest[:i]
}
if net.ParseIP(host) != nil && strings.Contains(host, ":") {
return prefix + "[" + host + "]" + rest[len(host):]
}
return input
}
// parseHostname will attempt to convert a user provided URL string into a string with some light error checking on
// certain expectations from the URL.
// Will convert all HTTP URLs to HTTPS
@@ -33,6 +52,7 @@ func parseURL(input string) (*url.URL, error) {
if !strings.HasPrefix(input, "https://") && !strings.HasPrefix(input, "http://") {
input = fmt.Sprintf("https://%s", input)
}
input = bracketBareIPv6(input)
url, err := url.ParseRequestURI(input)
if err != nil {
return nil, fmt.Errorf("failed to parse as URL: %w", err)
+28 -3
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseRequestHeaders(t *testing.T) {
@@ -15,6 +16,30 @@ func TestParseRequestHeaders(t *testing.T) {
assert.Equal(t, "000:000:0:1:asd", values.Get("cf-trace-id"))
}
func TestBracketBareIPv6(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"https://::1", "https://[::1]"},
{"https://::1/path", "https://[::1]/path"},
{"https://::1:8080", "https://[::1:8080]"},
{"https://::1:8080/path", "https://[::1:8080]/path"},
{"https://::1?query=1", "https://[::1]?query=1"}, // query without path
{"https://::1#fragment", "https://[::1]#fragment"}, // fragment without path
{"https://[::1]", "https://[::1]"}, // already bracketed
{"https://[::1]:8080", "https://[::1]:8080"}, // already bracketed with port
{"https://127.0.0.1", "https://127.0.0.1"}, // IPv4 unchanged
{"https://example.com", "https://example.com"}, // hostname unchanged
{"https://example.com:8080", "https://example.com:8080"}, // hostname:port unchanged
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.expected, bracketBareIPv6(tt.input))
})
}
}
func TestParseURL(t *testing.T) {
schemes := []string{
"http://",
@@ -28,8 +53,8 @@ func TestParseURL(t *testing.T) {
{"localhost", "localhost"},
{"127.0.0.1", "127.0.0.1"},
{"127.0.0.1:9090", "127.0.0.1:9090"},
{"::1", "::1"},
{"::1:8080", "::1:8080"},
{"::1", "[::1]"},
{"::1:8080", "[::1:8080]"},
{"[::1]", "[::1]"},
{"[::1]:8080", "[::1]:8080"},
{":8080", ":8080"},
@@ -49,7 +74,7 @@ func TestParseURL(t *testing.T) {
input := fmt.Sprintf("%s%s%s", scheme, host.input, path)
expected := fmt.Sprintf("%s%s%s", "https://", host.expected, path)
url, err := parseURL(input)
assert.NoError(t, err, "input: %s\texpected: %s", input, expected)
require.NoError(t, err, "input: %s\texpected: %s", input, expected)
assert.Equal(t, expected, url.String())
assert.Equal(t, host.expected, url.Host)
assert.Equal(t, "https", url.Scheme)
-87
View File
@@ -1,87 +0,0 @@
package main
import (
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/tunneldns"
)
const (
// ResolverServiceType is used to identify what kind of overwatch service this is
ResolverServiceType = "resolver"
LogFieldResolverAddress = "resolverAddress"
LogFieldResolverPort = "resolverPort"
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
)
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
// into a service model for the overwatch package.
// it also holds a reference to the config object that represents its state
type ResolverService struct {
resolver config.DNSResolver
shutdown chan struct{}
log *zerolog.Logger
}
// NewResolverService creates a new resolver service
func NewResolverService(r config.DNSResolver, log *zerolog.Logger) *ResolverService {
return &ResolverService{resolver: r,
shutdown: make(chan struct{}),
log: log,
}
}
// Name is used to figure out this service is related to the others (normally the addr it binds to)
// this is just "resolver" since there can only be one DNS resolver running
func (s *ResolverService) Name() string {
return ResolverServiceType
}
// Type is used to identify what kind of overwatch service this is
func (s *ResolverService) Type() string {
return ResolverServiceType
}
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
func (s *ResolverService) Hash() string {
return s.resolver.Hash()
}
// Shutdown stops the tunneldns listener
func (s *ResolverService) Shutdown() {
s.shutdown <- struct{}{}
}
// Run is the run loop that is started by the overwatch service
func (s *ResolverService) Run() error {
// create a listener
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
if err != nil {
return err
}
// start the listener.
readySignal := make(chan struct{})
err = l.Start(readySignal)
if err != nil {
_ = l.Stop()
return err
}
<-readySignal
resolverLog := s.log.With().
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
Logger()
resolverLog.Info().Msg("Starting resolver")
// wait for shutdown signal
<-s.shutdown
resolverLog.Info().Msg("Shutting down resolver")
return l.Stop()
}
+1 -9
View File
@@ -8,7 +8,7 @@ import (
)
// AppService is the main service that runs when no command lines flags are passed to cloudflared
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
// it manages all the running services such as tunnels, forwarders, etc
type AppService struct {
configManager config.Manager
serviceManager overwatch.Manager
@@ -73,14 +73,6 @@ func (s *AppService) handleConfigUpdate(c config.Root) {
activeServices[service.Name()] = struct{}{}
}
// handle resolver changes
if c.Resolver.Enabled {
service := NewResolverService(c.Resolver, s.log)
s.serviceManager.Add(service)
activeServices[service.Name()] = struct{}{}
}
// TODO: TUN-1451 - tunnels
// remove any services that are no longer active
+84
View File
@@ -0,0 +1,84 @@
package cliutil
import (
"errors"
"fmt"
"io"
"os"
"time"
"github.com/google/uuid"
"github.com/mattn/go-colorable"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials"
)
// Error definitions for management token operations
var (
ErrNoTunnelID = errors.New("no tunnel ID provided")
ErrInvalidTunnelID = errors.New("unable to parse provided tunnel id as a valid UUID")
)
// GetManagementToken acquires a management token from Cloudflare API for the specified resource
func GetManagementToken(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource, buildInfo *BuildInfo) (string, error) {
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
if err != nil {
return "", err
}
var apiURL string
if userCreds.IsFEDEndpoint() {
apiURL = credentials.FedRampBaseApiURL
} else {
apiURL = c.String(cfdflags.ApiURL)
}
client, err := userCreds.Client(apiURL, buildInfo.UserAgent(), log)
if err != nil {
return "", err
}
tunnelIDString := c.Args().First()
if tunnelIDString == "" {
return "", ErrNoTunnelID
}
tunnelID, err := uuid.Parse(tunnelIDString)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrInvalidTunnelID, err)
}
token, err := client.GetManagementToken(tunnelID, res)
if err != nil {
return "", err
}
return token, nil
}
// CreateStderrLogger creates a logger that outputs to stderr to avoid interfering with stdout
func CreateStderrLogger(c *cli.Context) *zerolog.Logger {
level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel))
if levelErr != nil {
level = zerolog.InfoLevel
}
var writer io.Writer
switch c.String(cfdflags.LogFormatOutput) {
case cfdflags.LogFormatOutputValueJSON:
// zerolog by default outputs as JSON
writer = os.Stderr
case cfdflags.LogFormatOutputValueDefault:
// "default" and unset use the same logger output format
fallthrough
default:
writer = zerolog.ConsoleWriter{
Out: colorable.NewColorable(os.Stderr),
TimeFormat: time.RFC3339,
}
}
log := zerolog.New(writer).With().Timestamp().Logger().Level(level)
return &log
}
-3
View File
@@ -111,9 +111,6 @@ const (
// ICMPV6Src is the command line flag to set the source address and the interface name to send/receive ICMPv6 messages
ICMPV6Src = "icmpv6-src"
// ProxyDns is the command line flag to run DNS server over HTTPS
ProxyDns = "proxy-dns"
// Name is the command line to set the name of the tunnel
Name = "name"
+4 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/cmd/cloudflared/management"
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tail"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
@@ -91,6 +92,7 @@ func main() {
tracing.Init(Version)
token.Init(Version)
tail.Init(bInfo)
management.Init(bInfo)
runApp(app, graceShutdownC)
}
@@ -149,9 +151,10 @@ To determine if an update happened in a script, check for error code 11.`,
},
}
cmds = append(cmds, tunnel.Commands()...)
cmds = append(cmds, proxydns.Command(false))
cmds = append(cmds, proxydns.Command()) // removed feature, only here for error message
cmds = append(cmds, access.Commands()...)
cmds = append(cmds, tail.Command())
cmds = append(cmds, management.Command())
return cmds
}
+105
View File
@@ -0,0 +1,105 @@
package management
import (
"encoding/json"
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials"
)
var buildInfo *cliutil.BuildInfo
// Init initializes the management package with build info
func Init(bi *cliutil.BuildInfo) {
buildInfo = bi
}
// Command returns the management command with its subcommands
func Command() *cli.Command {
return &cli.Command{
Name: "management",
Usage: "Monitor cloudflared tunnels via management API",
Category: "Management",
Hidden: true,
Subcommands: []*cli.Command{
buildTokenSubcommand(),
},
}
}
// buildTokenSubcommand creates the token subcommand
func buildTokenSubcommand() *cli.Command {
return &cli.Command{
Name: "token",
Action: cliutil.ConfiguredAction(tokenCommand),
Usage: "Get management access jwt for a specific resource",
UsageText: "cloudflared management token --resource <resource> TUNNEL_ID",
Description: "Get management access jwt for a tunnel with specified resource permissions (logs, admin, host_details)",
Hidden: true,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "resource",
Usage: "Resource type for token permissions: logs, admin, or host_details",
Required: true,
},
&cli.StringFlag{
Name: cfdflags.OriginCert,
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
Value: credentials.FindDefaultOriginCertPath(),
},
&cli.StringFlag{
Name: cfdflags.LogLevel,
Value: "info",
Usage: "Application logging level {debug, info, warn, error, fatal}",
EnvVars: []string{"TUNNEL_LOGLEVEL"},
},
cliutil.FlagLogOutput,
},
}
}
// tokenCommand handles the token subcommand execution
func tokenCommand(c *cli.Context) error {
log := cliutil.CreateStderrLogger(c)
// Parse and validate resource flag
resourceStr := c.String("resource")
resource, err := parseResource(resourceStr)
if err != nil {
return fmt.Errorf("invalid resource '%s': %w", resourceStr, err)
}
// Get management token
token, err := cliutil.GetManagementToken(c, log, resource, buildInfo)
if err != nil {
return err
}
// Output JSON to stdout
tokenResponse := struct {
Token string `json:"token"`
}{Token: token}
return json.NewEncoder(os.Stdout).Encode(tokenResponse)
}
// parseResource converts resource string to ManagementResource enum
func parseResource(resource string) (cfapi.ManagementResource, error) {
switch resource {
case "logs":
return cfapi.Logs, nil
case "admin":
return cfapi.Admin, nil
case "host_details":
return cfapi.HostDetails, nil
default:
return 0, fmt.Errorf("must be one of: logs, admin, host_details")
}
}
+71
View File
@@ -0,0 +1,71 @@
package management
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/cfapi"
)
func TestParseResource_ValidResources(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expected cfapi.ManagementResource
}{
{"logs", cfapi.Logs},
{"admin", cfapi.Admin},
{"host_details", cfapi.HostDetails},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
result, err := parseResource(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseResource_InvalidResource(t *testing.T) {
t.Parallel()
invalid := []string{"invalid", "LOGS", "Admin", "", "metrics", "host-details"}
for _, input := range invalid {
t.Run(input, func(t *testing.T) {
t.Parallel()
_, err := parseResource(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "must be one of")
})
}
}
func TestCommandStructure(t *testing.T) {
t.Parallel()
cmd := Command()
assert.Equal(t, "management", cmd.Name)
assert.True(t, cmd.Hidden)
assert.Len(t, cmd.Subcommands, 1)
tokenCmd := cmd.Subcommands[0]
assert.Equal(t, "token", tokenCmd.Name)
assert.True(t, tokenCmd.Hidden)
// Verify required flags exist
var hasResourceFlag bool
for _, flag := range tokenCmd.Flags {
if flag.Names()[0] == "resource" {
hasResourceFlag = true
break
}
}
assert.True(t, hasResourceFlag, "token command should have --resource flag")
}
+35 -96
View File
@@ -1,115 +1,54 @@
package proxydns
import (
"context"
"net"
"os"
"os/signal"
"syscall"
"errors"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/tunneldns"
)
func Command(hidden bool) *cli.Command {
return &cli.Command{
Name: "proxy-dns",
Action: cliutil.ConfiguredAction(Run),
const removedMessage = "dns-proxy feature is no longer supported"
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
},
&cli.StringFlag{
Name: "address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
},
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
&cli.IntFlag{
Name: "port",
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
Value: 53,
EnvVars: []string{"TUNNEL_DNS_PORT"},
},
&cli.StringSliceFlag{
Name: "upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
&cli.StringSliceFlag{
Name: "bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
},
&cli.IntFlag{
Name: "max-upstream-conns",
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
Value: tunneldns.MaxUpstreamConnsDefault,
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: hidden,
func Command() *cli.Command {
return &cli.Command{
Name: "proxy-dns",
Action: cliutil.ConfiguredAction(Run),
Usage: removedMessage,
SkipFlagParsing: true,
}
}
// Run implements a foreground runner
func Run(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
err := errors.New(removedMessage)
log.Error().Msg("DNS Proxy is no longer supported since version 2026.2.0 (https://developers.cloudflare.com/changelog/2025-11-11-cloudflared-proxy-dns/). As an alternative consider using https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/dns-over-https-client/")
metricsListener, err := net.Listen("tcp", c.String("metrics"))
if err != nil {
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
}
go metrics.ServeMetrics(metricsListener, context.Background(), metrics.Config{}, log)
listener, err := tunneldns.CreateListener(
c.String("address"),
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
uint16(c.Int("port")),
c.StringSlice("upstream"),
c.StringSlice("bootstrap"),
c.Int("max-upstream-conns"),
log,
)
if err != nil {
log.Err(err).Msg("Failed to create the listeners")
return err
}
// Try to start the server
readySignal := make(chan struct{})
err = listener.Start(readySignal)
if err != nil {
log.Err(err).Msg("Failed to start the listeners")
return listener.Stop()
}
<-readySignal
// Wait for signal
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(signals)
<-signals
// Shut down server
err = listener.Stop()
if err != nil {
log.Err(err).Msg("failed to stop")
}
return err
}
// Old flags used by the proxy-dns command, only kept to not break any script that might be setting these flags
func ConfigureProxyDNSFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-dns",
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-port",
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proxy-dns-address",
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-upstream",
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-max-upstream-conns",
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-bootstrap",
}),
}
}
+7 -69
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
@@ -13,11 +12,11 @@ import (
"time"
"github.com/google/uuid"
"github.com/mattn/go-colorable"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"nhooyr.io/websocket"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials"
@@ -50,9 +49,9 @@ func buildTailManagementTokenSubcommand() *cli.Command {
}
func managementTokenCommand(c *cli.Context) error {
log := createLogger(c)
log := cliutil.CreateStderrLogger(c)
token, err := getManagementToken(c, log)
token, err := cliutil.GetManagementToken(c, log, cfapi.Logs, buildInfo)
if err != nil {
return err
}
@@ -161,31 +160,6 @@ func handleValidationError(resp *http.Response, log *zerolog.Logger) {
}
}
// logger will be created to emit only against the os.Stderr as to not obstruct with normal output from
// management requests
func createLogger(c *cli.Context) *zerolog.Logger {
level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel))
if levelErr != nil {
level = zerolog.InfoLevel
}
var writer io.Writer
switch c.String(cfdflags.LogFormatOutput) {
case cfdflags.LogFormatOutputValueJSON:
// zerolog by default outputs as JSON
writer = os.Stderr
case cfdflags.LogFormatOutputValueDefault:
// "default" and unset use the same logger output format
fallthrough
default:
writer = zerolog.ConsoleWriter{
Out: colorable.NewColorable(os.Stderr),
TimeFormat: time.RFC3339,
}
}
log := zerolog.New(writer).With().Timestamp().Logger().Level(level)
return &log
}
// parseFilters will attempt to parse provided filters to send to with the EventStartStreaming
func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
var level *management.LogLevel
@@ -230,49 +204,13 @@ func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
}, nil
}
// getManagementToken will make a call to the Cloudflare API to acquire a management token for the requested tunnel.
func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
if err != nil {
return "", err
}
var apiURL string
if userCreds.IsFEDEndpoint() {
apiURL = credentials.FedRampBaseApiURL
} else {
apiURL = c.String(cfdflags.ApiURL)
}
client, err := userCreds.Client(apiURL, buildInfo.UserAgent(), log)
if err != nil {
return "", err
}
tunnelIDString := c.Args().First()
if tunnelIDString == "" {
return "", errors.New("no tunnel ID provided")
}
tunnelID, err := uuid.Parse(tunnelIDString)
if err != nil {
return "", errors.New("unable to parse provided tunnel id as a valid UUID")
}
token, err := client.GetManagementToken(tunnelID)
if err != nil {
return "", err
}
return token, nil
}
// buildURL will build the management url to contain the required query parameters to authenticate the request.
func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
func buildURL(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource) (url.URL, error) {
var err error
token := c.String("token")
if token == "" {
token, err = getManagementToken(c, log)
token, err = cliutil.GetManagementToken(c, log, res, buildInfo)
if err != nil {
return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err)
}
@@ -323,7 +261,7 @@ func printJSON(log *management.Log, logger *zerolog.Logger) {
// Run implements a foreground runner
func Run(c *cli.Context) error {
log := createLogger(c)
log := cliutil.CreateStderrLogger(c)
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
@@ -345,7 +283,7 @@ func Run(c *cli.Context) error {
return nil
}
u, err := buildURL(c, log)
u, err := buildURL(c, log, cfapi.Logs)
if err != nil {
log.Err(err).Msg("unable to construct management request URL")
return nil
+31 -118
View File
@@ -39,12 +39,12 @@ import (
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstate"
"github.com/cloudflare/cloudflared/validation"
)
const (
//nolint:gosec // This is the Sentry DSN for cloudflared which is safe to be public
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
LogFieldCommand = "command"
@@ -115,12 +115,6 @@ var (
cfdflags.LogFile,
cfdflags.LogDirectory,
cfdflags.TraceOutput,
cfdflags.ProxyDns,
"proxy-dns-port",
"proxy-dns-address",
"proxy-dns-upstream",
"proxy-dns-max-upstream-conns",
"proxy-dns-bootstrap",
cfdflags.IsAutoUpdated,
cfdflags.Edge,
cfdflags.Region,
@@ -181,8 +175,7 @@ func Commands() []*cli.Command {
buildCleanupCommand(),
buildTokenCommand(),
buildDiagCommand(),
// for compatibility, allow following as tunnel subcommands
proxydns.Command(true),
proxydns.Command(), // removed feature, only here for error message
cliutil.RemovedCommand("db-connect"),
}
@@ -238,7 +231,7 @@ func TunnelCommand(c *cli.Context) error {
return err
}
// Run a adhoc named tunnel
// Run an adhoc named tunnel
// Allows for the creation, routing (optional), and startup of a tunnel in one command
// --name required
// --url or --hello-world required
@@ -248,8 +241,8 @@ func TunnelCommand(c *cli.Context) error {
if err != nil {
return errors.Wrap(err, "Invalid hostname provided")
}
url := c.String("url")
if url == hostname && url != "" && hostname != "" {
tunnelURL := c.String("url")
if tunnelURL == hostname && tunnelURL != "" && hostname != "" {
return fmt.Errorf("hostname and url shouldn't match. See --help for more information")
}
@@ -258,15 +251,14 @@ func TunnelCommand(c *cli.Context) error {
// Run a quick tunnel
// A unauthenticated named tunnel hosted on <random>.<quick-tunnels-service>.com
// We don't support running proxy-dns and a quick tunnel at the same time as the same process
shouldRunQuickTunnel := c.IsSet("url") || c.IsSet(ingress.HelloWorldFlag)
if !c.IsSet(cfdflags.ProxyDns) && c.String("quick-service") != "" && shouldRunQuickTunnel {
if c.String("quick-service") != "" && shouldRunQuickTunnel {
return RunQuickTunnel(sc)
}
// If user provides a config, check to see if they meant to use `tunnel run` instead
if ref := config.GetConfiguration().TunnelID; ref != "" {
return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
return fmt.Errorf("use `cloudflared tunnel run` to start tunnel %s", ref)
}
// Classic tunnel usage is no longer supported
@@ -274,16 +266,6 @@ func TunnelCommand(c *cli.Context) error {
return errDeprecatedClassicTunnel
}
if c.IsSet(cfdflags.ProxyDns) {
if shouldRunQuickTunnel {
return fmt.Errorf("running a quick tunnel with `proxy-dns` is not supported")
}
// NamedTunnelProperties are nil since proxy dns server does not need it.
// This is supported for legacy reasons: dns proxy server is not a tunnel and ideally should
// not run as part of cloudflared tunnel.
return StartServer(sc.c, buildInfo, nil, sc.log)
}
return errors.New(tunnelCmdErrorMessage)
}
@@ -364,12 +346,14 @@ func StartServer(
traceLog.Err(err).Msg("Failed to close temporary trace output file")
}
traceOutputFilepath := c.String(cfdflags.TraceOutput)
//nolint:gosec // File path is safe because it is explicitly provided by the user via the --trace-output flag
if err := os.Rename(tmpTraceFile.Name(), traceOutputFilepath); err != nil {
traceLog.
Err(err).
Str(LogFieldTraceOutputFilepath, traceOutputFilepath).
Msg("Failed to rename temporary trace output file")
} else {
//nolint:gosec // File path is safe, since it is created by os.CreateTemp
err := os.Remove(tmpTraceFile.Name())
if err != nil {
traceLog.Err(err).Msg("Failed to remove the temporary trace file")
@@ -387,30 +371,18 @@ func StartServer(
info.Log(log)
logClientOptions(c, log)
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
// this context drives the server, when it's canceled tunnel and all other components (origins, dns, etc...) should stop
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
go waitForSignal(graceShutdownC, log)
if c.IsSet(cfdflags.ProxyDns) {
dnsReadySignal := make(chan struct{})
wg.Add(1)
go func() {
defer wg.Done()
errC <- runDNSProxyServer(c, dnsReadySignal, ctx.Done(), log)
}()
// Wait for proxy-dns to come up (if used)
<-dnsReadySignal
}
connectedSignal := signal.New(make(chan struct{}))
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"), log)
}
// update needs to be after DNS proxy is up to resolve equinox server address
wg.Add(1)
go func() {
defer wg.Done()
@@ -420,11 +392,8 @@ func StartServer(
errC <- autoupdater.Run(ctx)
}()
// Serve DNS proxy stand-alone if no tunnel type (quick, adhoc, named) is going to run
if dnsProxyStandAlone(c, namedTunnel) {
connectedSignal.Notify()
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, log)
if namedTunnel == nil {
return fmt.Errorf("namedTunnel is nil")
}
logTransport := logger.CreateTransportLoggerFromContext(c, logger.EnableTerminalLog)
@@ -432,10 +401,7 @@ func StartServer(
observer := connection.NewObserver(log, logTransport)
// Send Quick Tunnel URL to UI if applicable
var quickTunnelURL string
if namedTunnel != nil {
quickTunnelURL = namedTunnel.QuickTunnelUrl
}
quickTunnelURL := namedTunnel.QuickTunnelUrl
if quickTunnelURL != "" {
observer.SendURL(quickTunnelURL)
}
@@ -459,14 +425,7 @@ func StartServer(
}
}
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
var isFEDEndpoint bool
if err != nil {
isFEDEndpoint = false
} else {
isFEDEndpoint = userCreds.IsFEDEndpoint()
}
isFEDEndpoint := namedTunnel.Credentials.Endpoint == credentials.FedEndpoint
var managementHostname string
if isFEDEndpoint {
managementHostname = credentials.FedRampHostname
@@ -495,7 +454,7 @@ func StartServer(
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
defer func() { _ = metricsListener.Close() }()
wg.Add(1)
go func() {
@@ -609,13 +568,14 @@ func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog
log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile")
return
}
file, err := os.Create(expandedPath)
cleanPath := filepath.Clean(expandedPath)
file, err := os.Create(cleanPath)
if err != nil {
log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid")
return
}
defer file.Close()
fmt.Fprintf(file, "%d", os.Getpid())
defer func() { _ = file.Close() }()
_, _ = fmt.Fprintf(file, "%d", os.Getpid())
}
func hostnameFromURI(uri string) string {
@@ -647,7 +607,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
flags := configureCloudflaredFlags(shouldHide)
flags = append(flags, configureProxyFlags(shouldHide)...)
flags = append(flags, cliutil.ConfigureLoggingFlags(shouldHide)...)
flags = append(flags, configureProxyDNSFlags(shouldHide)...)
flags = append(flags, proxydns.ConfigureProxyDNSFlags(shouldHide)...) // removed feature, only kept to not break any script that might be setting these flags
flags = append(flags, []cli.Flag{
credentialsFileFlag,
altsrc.NewBoolFlag(&cli.BoolFlag{
@@ -671,7 +631,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Name: cfdflags.EdgeIpVersion,
Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}",
EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"},
Value: "4",
Value: "auto",
Hidden: false,
}),
altsrc.NewStringFlag(&cli.StringFlag{
@@ -1178,58 +1138,13 @@ func sshFlags(shouldHide bool) []cli.Flag {
}
}
func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: cfdflags.ProxyDns,
Usage: "Run a DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-port",
Value: 53,
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS_PORT"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proxy-dns-address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
Hidden: shouldHide,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-max-upstream-conns",
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
Value: tunneldns.MaxUpstreamConnsDefault,
Hidden: shouldHide,
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice(
"https://162.159.36.1/dns-query",
"https://162.159.46.1/dns-query",
"https://[2606:4700:4700::1111]/dns-query",
"https://[2606:4700:4700::1001]/dns-query",
),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
Hidden: shouldHide,
}),
}
}
func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logger) {
helpStr := strings.Join([]string{
"Supported command:",
"reconnect [delay]",
"- restarts one randomly chosen connection with optional delay before reconnect\n",
}, "\n")
for {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
@@ -1238,7 +1153,7 @@ func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logg
switch parts[0] {
case "":
break
continue
case "reconnect":
var reconnect supervisor.ReconnectSignal
if len(parts) > 1 {
@@ -1250,13 +1165,11 @@ func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logg
}
log.Info().Msgf("Sending %+v", reconnect)
reconnectCh <- reconnect
case "help":
log.Info().Msg(helpStr)
default:
log.Info().Str(LogFieldCommand, command).Msg("Unknown command")
fallthrough
case "help":
log.Info().Msg(`Supported command:
reconnect [delay]
- restarts one randomly chosen connection with optional delay before reconnect`)
log.Info().Msg(helpStr)
}
}
}
-7
View File
@@ -111,13 +111,6 @@ func isSecretEnvVar(key string) bool {
return false
}
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.TunnelProperties) bool {
return c.IsSet(flags.ProxyDns) &&
!(c.IsSet(flags.Name) || // adhoc-named tunnel
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
namedTunnel != nil) // named tunnel
}
func prepareTunnelConfig(
ctx context.Context,
c *cli.Context,
+8 -6
View File
@@ -63,12 +63,14 @@ func (s searchByID) Path() (string, error) {
Str("originCertPath", originCertPath).
Logger()
// Fallback to look for tunnel credentials in the origin cert directory
if originCertPath, err := credentials.FindOriginCert(originCertPath, &originCertLog); err == nil {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
if s.fs.validFilePath(filePath) {
return filePath, nil
if originCertPath != "" {
// Look for tunnel credentials in the origin cert directory if the flag is provided
if originCertPath, err := credentials.FindOriginCert(originCertPath, &originCertLog); err == nil {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(s.id, originCertDir); err == nil {
if s.fs.validFilePath(filePath) {
return filePath, nil
}
}
}
}
-37
View File
@@ -1,37 +0,0 @@
package tunnel
import (
"fmt"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
)
func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <-chan struct{}, log *zerolog.Logger) error {
port := c.Int("proxy-dns-port")
if port <= 0 || port > 65535 {
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
}
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
if maxUpstreamConnections < 0 {
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
}
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
if err != nil {
close(dnsReadySignal)
listener.Stop()
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
}
err = listener.Start(dnsReadySignal)
if err != nil {
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
}
<-shutdownC
_ = listener.Stop()
log.Info().Msg("DNS server stopped")
return nil
}
+23 -6
View File
@@ -30,7 +30,7 @@ class CloudflaredCli:
listed = self._run_command(cmd_args, "list")
return json.loads(listed.stdout)
def get_management_token(self, config, config_path):
def get_management_token(self, config, config_path, resource):
basecmd = [config.cloudflared_binary]
if config_path is not None:
basecmd += ["--config", str(config_path)]
@@ -38,18 +38,35 @@ class CloudflaredCli:
if origincert:
basecmd += ["--origincert", origincert]
cmd_args = ["tail", "token", config.get_tunnel_id()]
cmd_args = ["management", "token", "--resource", resource, config.get_tunnel_id()]
cmd = basecmd + cmd_args
result = run_subprocess(cmd, "token", self.logger, check=True, capture_output=True, timeout=15)
return json.loads(result.stdout.decode("utf-8").strip())["token"]
def get_management_url(self, path, config, config_path):
access_jwt = self.get_management_token(config, config_path)
def get_tail_token(self, config, config_path):
"""
Get management token using the 'tail token' command.
Returns a token scoped for 'logs' resource.
"""
basecmd = [config.cloudflared_binary]
if config_path is not None:
basecmd += ["--config", str(config_path)]
origincert = get_config_from_file()["origincert"]
if origincert:
basecmd += ["--origincert", origincert]
cmd_args = ["tail", "token", config.get_tunnel_id()]
cmd = basecmd + cmd_args
result = run_subprocess(cmd, "tail-token", self.logger, check=True, capture_output=True, timeout=15)
return json.loads(result.stdout.decode("utf-8").strip())["token"]
def get_management_url(self, path, config, config_path, resource):
access_jwt = self.get_management_token(config, config_path, resource)
connector_id = get_tunnel_connector_id()
return f"https://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
def get_management_wsurl(self, path, config, config_path):
access_jwt = self.get_management_token(config, config_path)
def get_management_wsurl(self, path, config, config_path, resource):
access_jwt = self.get_management_token(config, config_path, resource)
connector_id = get_tunnel_connector_id()
return f"wss://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
+1 -8
View File
@@ -5,7 +5,7 @@ import base64
from dataclasses import dataclass, InitVar
from constants import METRICS_PORT, PROXY_DNS_PORT
from constants import METRICS_PORT
# frozen=True raises exception when assigning to fields. This emulates immutability
@@ -99,10 +99,3 @@ class QuickTunnelConfig(BaseConfig):
object.__setattr__(self, 'full_config',
self.merge_config(additional_config))
@dataclass(frozen=True)
class ProxyDnsConfig(BaseConfig):
full_config = {
"port": PROXY_DNS_PORT,
"no-autoupdate": True,
}
+3 -15
View File
@@ -5,15 +5,14 @@ from time import sleep
import pytest
import yaml
from config import NamedTunnelConfig, ProxyDnsConfig, QuickTunnelConfig
from constants import BACKOFF_SECS, PROXY_DNS_PORT
from config import NamedTunnelConfig, QuickTunnelConfig
from constants import BACKOFF_SECS
from util import LOGGER
class CfdModes(Enum):
NAMED = auto()
QUICK = auto()
PROXY_DNS = auto()
@pytest.fixture(scope="session")
@@ -26,16 +25,7 @@ def component_tests_config():
config = yaml.safe_load(stream)
LOGGER.info(f"component tests base config {config}")
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True, provide_ingress=True):
if run_proxy_dns:
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
# So we run all tests with it.
additional_config["proxy-dns"] = True
additional_config["proxy-dns-port"] = PROXY_DNS_PORT
else:
additional_config.pop("proxy-dns", None)
additional_config.pop("proxy-dns-port", None)
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, provide_ingress=True):
# Allows the ingress rules to be omitted from the provided config
ingress = []
if provide_ingress:
@@ -51,8 +41,6 @@ def component_tests_config():
credentials_file=config['credentials_file'],
ingress=ingress,
hostname=hostname)
elif cfd_mode is CfdModes.PROXY_DNS:
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
elif cfd_mode is CfdModes.QUICK:
return QuickTunnelConfig(additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'])
else:
-1
View File
@@ -3,7 +3,6 @@ MAX_RETRIES = 5
BACKOFF_SECS = 7
MAX_LOG_LINES = 50
PROXY_DNS_PORT = 9053
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
+13 -4
View File
@@ -20,12 +20,21 @@ class TestEdgeDiscovery:
@pytest.mark.parametrize("protocol", protocols())
def test_default_only(self, tmp_path, component_tests_config, protocol):
"""
This test runs a tunnel to connect via IPv4-only edge addresses (default is unset "--edge-ip-version 4")
This test runs a tunnel with the default edge-ip-version (auto), which will use
whichever address family the system resolver returns first.
"""
if self.has_ipv6_only():
pytest.skip("Host has IPv6 only support and current default is IPv4 only")
self.expect_address_connections(
tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
self.expect_address_connections(
tmp_path, component_tests_config, protocol, None, self.expect_ipv6_address)
elif self.has_ipv4_only():
self.expect_address_connections(
tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
elif self.has_dual_stack(address_family_preference=socket.AddressFamily.AF_INET6):
self.expect_address_connections(
tmp_path, component_tests_config, protocol, None, self.expect_ipv6_address)
else:
self.expect_address_connections(
tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
@pytest.mark.parametrize("protocol", protocols())
def test_ipv4_only(self, tmp_path, component_tests_config, protocol):
+43 -9
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python
import json
import requests
from conftest import CfdModes
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
from retrying import retry
from cli import CloudflaredCli
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests, decode_jwt_payload
import platform
"""
@@ -25,7 +26,7 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
headers = {}
headers["Content-Type"] = "application/json"
@@ -35,7 +36,7 @@ class TestManagement:
require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
connector_id = cfd_cli.get_connector_id(config)[0]
url = cfd_cli.get_management_url("host_details", config, config_path)
url = cfd_cli.get_management_url("host_details", config, config_path, resource="host_details")
resp = send_request(url, headers=headers)
# Assert response json.
@@ -52,13 +53,13 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("metrics", config, config_path)
url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin")
resp = send_request(url)
# Assert response.
@@ -73,13 +74,13 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path)
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path, resource="admin")
resp = send_request(url)
# Assert response.
@@ -94,18 +95,51 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("metrics", config, config_path)
url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin")
resp = send_request(url)
# Assert response.
assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics"
def test_tail_token_command(self, tmp_path, component_tests_config):
"""
Validates that 'cloudflared tail token' command returns a token
scoped for 'logs' and 'ping' resources.
"""
# TUN-7377: wait_tunnel_ready does not work properly in windows
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
token = cfd_cli.get_tail_token(config, config_path)
# Verify token was returned
assert token, "Expected non-empty token to be returned"
# Decode JWT payload to verify resource claims
claims = decode_jwt_payload(token)
resource_tag = 'res'
# Verify the token has 'logs' and 'ping' in resource array
assert resource_tag in claims, f"Expected {resource_tag} claim in token"
assert isinstance(claims['res'], list), f"Expected {resource_tag} to be an array"
assert 'logs' in claims[resource_tag], \
f"Expected 'logs' in resource array, got: {claims[resource_tag]}"
assert 'ping' in claims[resource_tag], \
f"Expected 'ping' in resource array, got: {claims[resource_tag]}"
LOGGER.info(f"Tail token successfully verified with resources: {claims[resource_tag]}")
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env python
import socket
from time import sleep
import constants
from conftest import CfdModes
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
# Sanity checks that test that we only run Proxy DNS and Tunnel when we really expect them to be there.
class TestProxyDns:
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
expect_proxy_dns = run_proxy_dns
expect_tunnel = False
if cfd_mode == CfdModes.NAMED:
expect_tunnel = True
pre_args = ["tunnel", "--ha-connections", "1"]
args = ["run"]
elif cfd_mode == CfdModes.PROXY_DNS:
expect_proxy_dns = True
pre_args = []
args = ["proxy-dns", "--port", str(constants.PROXY_DNS_PORT)]
else:
assert False, f"Unknown cfd_mode {cfd_mode}"
config = component_tests_config(cfd_mode=cfd_mode, run_proxy_dns=run_proxy_dns)
with start_cloudflared(tmp_path, config, cfd_pre_args=pre_args, cfd_args=args, new_process=True, capture_output=False):
if expect_tunnel:
wait_tunnel_ready()
else:
check_tunnel_not_connected()
verify_proxy_dns(expect_proxy_dns)
def verify_proxy_dns(should_be_running):
# Wait for the Proxy DNS listener to come up.
sleep(constants.BACKOFF_SECS)
had_failure = False
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(('localhost', constants.PROXY_DNS_PORT))
sock.send(b"anything")
except:
if should_be_running:
assert False, "Expected Proxy DNS to be running, but it was not."
had_failure = True
finally:
sock.close()
if not should_be_running and not had_failure:
assert False, "Proxy DNS should not have been running, but it was."
+2 -14
View File
@@ -6,7 +6,7 @@ from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_u
class TestQuickTunnels:
def test_quick_tunnel(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
config = component_tests_config(cfd_mode=CfdModes.QUICK)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
@@ -15,22 +15,10 @@ class TestQuickTunnels:
send_requests(url, 3, True)
def test_quick_tunnel_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
config = component_tests_config(cfd_mode=CfdModes.QUICK)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
time.sleep(10)
url = get_quicktunnel_url()
send_requests(url+"/ready", 3, True)
def test_quick_tunnel_proxy_dns_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
LOGGER.debug(config)
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], expect_success=False)
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `hello-world`"
def test_quick_tunnel_proxy_dns_hello_world(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
LOGGER.debug(config)
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--hello-world"], expect_success=False)
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `url`"
+10 -10
View File
@@ -19,13 +19,13 @@ class TestTail:
with the access token and start and stop streaming on-demand.
"""
print("test_start_stop_streaming")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=3) as websocket:
await websocket.send('{"type": "start_streaming"}')
await websocket.send('{"type": "stop_streaming"}')
@@ -38,13 +38,13 @@ class TestTail:
Validates that a streaming logs connection will stream logs
"""
print("test_streaming_logs")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# send start_streaming
await websocket.send(json.dumps({
@@ -65,13 +65,13 @@ class TestTail:
but not http when filters applied.
"""
print("test_streaming_logs_filters")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# send start_streaming with tcp logs only
await websocket.send(json.dumps({
@@ -92,13 +92,13 @@ class TestTail:
Validates that a streaming logs connection will stream logs with sampling.
"""
print("test_streaming_logs_sampling")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# send start_streaming with info logs only
await websocket.send(json.dumps({
@@ -120,13 +120,13 @@ class TestTail:
Validates that a streaming logs session can be overriden by the same actor
"""
print("test_streaming_logs_actor_override")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
task = asyncio.ensure_future(start_streaming_to_be_remotely_closed(url))
override_task = asyncio.ensure_future(start_streaming_override(url))
await asyncio.wait([task, override_task])
+17 -10
View File
@@ -11,14 +11,14 @@ class TestTunnel:
'''Test tunnels with no ingress rules from config.yaml but ingress rules from CLI only'''
def test_tunnel_hello_world(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=1)
def test_tunnel_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
@@ -29,17 +29,24 @@ class TestTunnel:
Running a tunnel with no ingress rules provided from either config.yaml or CLI will still work but return 503
for all incoming requests.
'''
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
resp = send_request(config.get_url()+"/")
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
resp = send_request(config.get_url()+"/test")
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
expected_status_code = 503
resp = send_request(config.get_url()+"/", expected_status_code)
assert resp.status_code == expected_status_code, "Expected cloudflared to return 503 for all requests with no ingress defined"
resp = send_request(config.get_url()+"/test", expected_status_code)
assert resp.status_code == expected_status_code, "Expected cloudflared to return 503 for all requests with no ingress defined"
def retry_if_result_none(result):
'''
Returns True if the result is None, indicating that the function should be retried.
'''
return result is None
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def send_request(url, headers={}):
@retry(retry_on_result=retry_if_result_none, stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def send_request(url, expected_status_code=200):
with requests.Session() as s:
return s.get(url, timeout=BACKOFF_SECS, headers=headers)
resp = s.get(url, timeout=BACKOFF_SECS)
return resp if resp.status_code == expected_status_code else None
+46
View File
@@ -185,3 +185,49 @@ def send_request(session, url, require_ok):
if require_ok:
assert resp.status_code == 200, f"{url} returned {resp}"
return resp if resp.status_code == 200 else None
def decode_jwt_payload(token):
"""
Decode the payload section of a JWT token without signature verification.
JWT Structure:
==============
A JWT consists of three Base64URL-encoded parts separated by dots:
HEADER.PAYLOAD.SIGNATURE
The payload contains the JWT claims (the actual data/permissions).
Args:
token (str): The complete JWT token string
Returns:
dict: The decoded payload as a dictionary containing JWT claims
Raises:
ValueError: If the token doesn't have exactly 3 parts
Note:
This function does NOT verify the signature - it only decodes the payload.
Use this only when you trust the token source (e.g., tokens you just generated).
"""
import base64
import json
# Split JWT into its three components
parts = token.split('.')
if len(parts) != 3:
raise ValueError(f"Invalid JWT format: expected 3 parts, got {len(parts)}")
# Extract and decode the payload (middle section)
# Base64 requires padding to be a multiple of 4 characters
payload_encoded = parts[1]
remainder = len(payload_encoded) % 4
if remainder != 0:
payload_padded = payload_encoded + '=' * (4 - remainder)
else:
payload_padded = payload_encoded
# Decode from Base64URL format and parse JSON
decoded_payload = base64.urlsafe_b64decode(payload_padded)
return json.loads(decoded_payload)
+2 -72
View File
@@ -4,9 +4,6 @@ import (
"crypto/sha256"
"fmt"
"io"
"strings"
"github.com/cloudflare/cloudflared/tunneldns"
)
// Forwarder represents a client side listener to forward traffic to the edge
@@ -26,23 +23,13 @@ type Tunnel struct {
ProtocolType string `json:"type"`
}
// DNSResolver represents a client side DNS resolver
type DNSResolver struct {
Enabled bool `json:"enabled"`
Address string `json:"address,omitempty"`
Port uint16 `json:"port,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"`
MaxUpstreamConnections int `json:"max_upstream_connections,omitempty"`
}
// Root is the base options to configure the service
// Root is the base options to configure the service.
type Root struct {
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
// `resolver` key is reserved for a removed feature (proxy-dns) and should not be used.
}
// Hash returns the computed values to see if the forwarder values change
@@ -55,60 +42,3 @@ func (f *Forwarder) Hash() string {
_, _ = io.WriteString(h, f.Destination)
return fmt.Sprintf("%x", h.Sum(nil))
}
// Hash returns the computed values to see if the forwarder values change
func (r *DNSResolver) Hash() string {
h := sha256.New()
_, _ = io.WriteString(h, r.Address)
_, _ = io.WriteString(h, strings.Join(r.Bootstraps, ","))
_, _ = io.WriteString(h, strings.Join(r.Upstreams, ","))
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.Port))
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections))
_, _ = io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
return fmt.Sprintf("%x", h.Sum(nil))
}
// EnabledOrDefault returns the enabled property
func (r *DNSResolver) EnabledOrDefault() bool {
return r.Enabled
}
// AddressOrDefault returns the address or returns the default if empty
func (r *DNSResolver) AddressOrDefault() string {
if r.Address != "" {
return r.Address
}
return "localhost"
}
// PortOrDefault return the port or returns the default if 0
func (r *DNSResolver) PortOrDefault() uint16 {
if r.Port > 0 {
return r.Port
}
return 53
}
// UpstreamsOrDefault returns the upstreams or returns the default if empty
func (r *DNSResolver) UpstreamsOrDefault() []string {
if len(r.Upstreams) > 0 {
return r.Upstreams
}
return []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"}
}
// BootstrapsOrDefault returns the bootstraps or returns the default if empty
func (r *DNSResolver) BootstrapsOrDefault() []string {
if len(r.Bootstraps) > 0 {
return r.Bootstraps
}
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
}
// MaxUpstreamConnectionsOrDefault return the max upstream connections or returns the default if negative
func (r *DNSResolver) MaxUpstreamConnectionsOrDefault() int {
if r.MaxUpstreamConnections >= 0 {
return r.MaxUpstreamConnections
}
return tunneldns.MaxUpstreamConnsDefault
}
+1 -1
View File
@@ -295,7 +295,7 @@ func TestServeWS(t *testing.T) {
require.False(t, respWriter.panicked)
}
// TestNoWriteAfterServeHTTPReturns is a regression test of https://jira.cfops.it/browse/TUN-5184
// TestNoWriteAfterServeHTTPReturns is a regression test of https://jira.cfdata.org/browse/TUN-5184
// to make sure we don't write to the ResponseWriter after the ServeHTTP method returns
func TestNoWriteAfterServeHTTPReturns(t *testing.T) {
cfdHTTP2Conn, edgeTCPConn := newTestHTTP2Connection()
+21 -1
View File
@@ -19,6 +19,9 @@ const (
edgeH2TLSServerName = "h2.cftunnel.com"
// edgeQUICServerName is the server name to establish quic connection with edge.
edgeQUICServerName = "quic.cftunnel.com"
// probeTLSServerName is the server name used for pre-flight connectivity checks.
probeTLSServerName = "probe.cftunnel.com"
quicProtos = "argotunnel"
AutoSelectFlag = "auto"
// SRV and TXT record resolution TTL
ResolveTTL = time.Hour
@@ -69,7 +72,24 @@ func (p Protocol) TLSSettings() *TLSSettings {
case QUIC:
return &TLSSettings{
ServerName: edgeQUICServerName,
NextProtos: []string{"argotunnel"},
NextProtos: []string{quicProtos},
}
default:
return nil
}
}
// ProbeTLSSettings returns TLS settings for pre-flight connectivity checks.
func (p Protocol) ProbeTLSSettings() *TLSSettings {
switch p {
case HTTP2:
return &TLSSettings{
ServerName: probeTLSServerName,
}
case QUIC:
return &TLSSettings{
ServerName: probeTLSServerName,
NextProtos: []string{quicProtos},
}
default:
return nil
+50 -17
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/edgediscovery"
)
@@ -14,15 +15,6 @@ const (
testAccountTag = "testAccountTag"
)
func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) edgediscovery.PercentageFetcher {
return func() (edgediscovery.ProtocolPercents, error) {
if getError {
return nil, fmt.Errorf("failed to fetch percentage")
}
return protocolPercent, nil
}
}
type dynamicMockFetcher struct {
protocolPercents edgediscovery.ProtocolPercents
err error
@@ -89,14 +81,14 @@ func TestNewProtocolSelector(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
selector, err := NewProtocolSelector(test.protocol, testAccountTag, test.tunnelTokenProvided, test.needPQ, fetcher.fetch(), ResolveTTL, &log)
if test.wantErr {
assert.Error(t, err, fmt.Sprintf("test %s failed", test.name))
assert.Error(t, err, "test %s failed", test.name)
} else {
assert.NoError(t, err, fmt.Sprintf("test %s failed", test.name))
assert.Equal(t, test.expectedProtocol, selector.Current(), fmt.Sprintf("test %s failed", test.name))
require.NoError(t, err, "test %s failed", test.name)
assert.Equalf(t, test.expectedProtocol, selector.Current(), "test %s failed", test.name)
fallback, ok := selector.Fallback()
assert.Equal(t, test.hasFallback, ok, fmt.Sprintf("test %s failed", test.name))
assert.Equalf(t, test.hasFallback, ok, "test %s failed", test.name)
if test.hasFallback {
assert.Equal(t, test.expectedFallback, fallback, fmt.Sprintf("test %s failed", test.name))
assert.Equalf(t, test.expectedFallback, fallback, "test %s failed", test.name)
}
}
})
@@ -106,7 +98,7 @@ func TestNewProtocolSelector(t *testing.T) {
func TestAutoProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, QUIC, selector.Current())
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
@@ -136,7 +128,7 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{}
// Since the user chooses http2 on purpose, we always stick to it.
selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, HTTP2, selector.Current())
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
@@ -165,9 +157,50 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
func TestAutoProtocolSelectorNoRefreshWithToken(t *testing.T) {
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, false, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, QUIC, selector.Current())
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
assert.Equal(t, QUIC, selector.Current())
}
func TestProbeTLSSettings(t *testing.T) {
tests := []struct {
name string
protocol Protocol
expectedServer string
expectedProtos []string
expectNil bool
}{
{
name: "HTTP2 returns probe SNI",
protocol: HTTP2,
expectedServer: probeTLSServerName,
expectedProtos: nil,
},
{
name: "QUIC returns probe SNI with alpn",
protocol: QUIC,
expectedServer: probeTLSServerName,
expectedProtos: []string{"argotunnel"},
},
{
name: "Unknown protocol returns nil",
protocol: Protocol(999),
expectNil: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
settings := test.protocol.ProbeTLSSettings()
if test.expectNil {
assert.Nil(t, settings)
} else {
assert.NotNil(t, settings)
assert.Equal(t, test.expectedServer, settings.ServerName)
assert.Equal(t, test.expectedProtos, settings.NextProtos)
}
})
}
}
+4 -2
View File
@@ -33,6 +33,8 @@ const (
HTTPMethodKey = "HttpMethod"
// HTTPHostKey is used to get or set http host in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPHostKey = "HttpHost"
// HTTPStatus is used to return http status code in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPStatus = "HttpStatus"
QUICMetadataFlowID = "FlowID"
)
@@ -287,7 +289,7 @@ func (hrw *httpResponseAdapter) AddTrailer(trailerName, trailerValue string) {
func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
metadata := make([]pogs.Metadata, 0)
metadata = append(metadata, pogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(status)})
metadata = append(metadata, pogs.Metadata{Key: HTTPStatus, Val: strconv.Itoa(status)})
for k, vv := range header {
for _, v := range vv {
httpHeaderKey := fmt.Sprintf("%s:%s", HTTPHeaderKey, k)
@@ -327,7 +329,7 @@ func (hrw *httpResponseAdapter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
}
func (hrw *httpResponseAdapter) WriteErrorResponse(err error) {
_ = hrw.WriteConnectResponseData(err, pogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
_ = hrw.WriteConnectResponseData(err, pogs.Metadata{Key: HTTPStatus, Val: strconv.Itoa(http.StatusBadGateway)})
}
func (hrw *httpResponseAdapter) WriteConnectResponseData(respErr error, metadata ...pogs.Metadata) error {
-2
View File
@@ -149,7 +149,6 @@ func TestQUICServer(t *testing.T) {
}
for i, test := range tests {
test := test // capture range variable
t.Run(test.desc, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
// Start a UDP Listener for QUIC.
@@ -499,7 +498,6 @@ func TestBuildHTTPRequest(t *testing.T) {
log := zerolog.Nop()
for _, test := range tests {
test := test // capture range variable
t.Run(test.name, func(t *testing.T) {
req, err := buildHTTPRequest(t.Context(), test.connectRequest, test.body, 0, &log)
require.NoError(t, err)
+12 -21
View File
@@ -1,45 +1,43 @@
module github.com/cloudflare/cloudflared
go 1.24
go 1.26
require (
github.com/coredns/coredns v1.12.2
github.com/coreos/go-oidc/v3 v3.10.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/coreos/go-systemd/v22 v22.5.0
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/getsentry/sentry-go v0.16.0
github.com/getsentry/sentry-go v0.43.0
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/cors v1.2.1
github.com/go-jose/go-jose/v4 v4.1.0
github.com/go-jose/go-jose/v4 v4.1.3
github.com/gobwas/ws v1.2.1
github.com/google/gopacket v1.1.19
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/json-iterator/go v1.1.12
github.com/mattn/go-colorable v0.1.13
github.com/miekg/dns v1.1.66
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.2
github.com/quic-go/quic-go v0.52.0
github.com/rs/zerolog v1.20.0
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.3.0
go.opentelemetry.io/contrib/propagators v0.22.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
go.opentelemetry.io/proto/otlp v1.2.0
go.uber.org/automaxprocs v1.6.0
go.uber.org/mock v0.5.1
golang.org/x/crypto v0.38.0
golang.org/x/net v0.40.0
golang.org/x/sync v0.14.0
golang.org/x/sys v0.33.0
golang.org/x/sys v0.40.0
golang.org/x/term v0.32.0
google.golang.org/protobuf v1.36.6
gopkg.in/natefinch/lumberjack.v2 v2.0.0
@@ -50,18 +48,15 @@ require (
require (
github.com/BurntSushi/toml v1.2.0 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -69,27 +64,23 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
github.com/tinylib/msgp v1.6.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
+28 -47
View File
@@ -1,8 +1,6 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
@@ -17,12 +15,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 h1:c+Epklw9xk6BZ1OFBPWLA2PcL8QalKvl3if8CP9x8uw=
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
github.com/coredns/coredns v1.12.2 h1:G4oDfi340zlVsriZ8nYiUemiQIew7nqOO+QPvPxIA4Y=
github.com/coredns/coredns v1.12.2/go.mod h1:GFz31oVOfCyMArFoypfu1SoaFoNkbdh6lDxtF1B6vfU=
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -43,16 +37,14 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getsentry/sentry-go v0.16.0 h1:owk+S+5XcgJLlGR/3+3s6N4d+uKwqYvh/eS0AIMjPWo=
github.com/getsentry/sentry-go v0.16.0/go.mod h1:ZXCloQLj0pG7mja5NK6NPf2V4A88YJ4pNlc2mOHwh6Y=
github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4=
github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
@@ -64,8 +56,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -95,7 +87,6 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -109,7 +100,6 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34=
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -117,8 +107,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -144,10 +132,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -162,12 +146,10 @@ github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -186,8 +168,8 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
@@ -204,10 +186,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
@@ -215,24 +197,24 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/propagators v0.22.0 h1:KGdv58M2//veiYLIhb31mofaI2LgkIPXXAZVeYVyfd8=
go.opentelemetry.io/contrib/propagators v0.22.0/go.mod h1:xGOuXr6lLIF9BXipA4pm6UuOSI0M98U6tsI3khbOiwU=
go.opentelemetry.io/otel v1.0.0-RC2/go.mod h1:w1thVQ7qbAy8MHb0IFj8a5Q2QU0l2ksf8u/CN8m3NOM=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
@@ -257,7 +239,6 @@ golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
@@ -268,8 +249,8 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+2 -2
View File
@@ -28,7 +28,7 @@ func FindProtocol(p []byte) (layers.IPProtocol, error) {
// Next header is in the 7th byte of IPv6 header
return layers.IPProtocol(p[6]), nil
default:
return 0, fmt.Errorf("unknow ip version %d", version)
return 0, fmt.Errorf("unknown ip version %d", version)
}
}
@@ -105,7 +105,7 @@ func (pd *IPDecoder) decodeByVersion(packet []byte) ([]gopacket.LayerType, error
case 6:
err = pd.v6parser.DecodeLayers(packet, &decoded)
default:
err = fmt.Errorf("unknow ip version %d", version)
err = fmt.Errorf("unknown ip version %d", version)
}
if err != nil {
return nil, err
+82
View File
@@ -0,0 +1,82 @@
package prechecks
import (
"context"
"crypto/tls"
"net"
"net/netip"
"time"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
)
// DNSResolver abstracts edge DNS discovery used by DNS probes.
//
// The production implementation wraps allregions.EdgeDiscovery
// (edgediscovery/allregions/discovery.go), which performs an SRV lookup for
// _v2-origintunneld._tcp.argotunnel.com, falls back to DNS-over-TLS when the
// system resolver fails, and resolves each discovered hostname via
// net.LookupIP. The returned slice already has each address tagged with
// .IPVersion = V4 or V6.
//
// Note: allregions.EdgeDiscovery must be exported (currently unexported as
// edgeDiscovery) before a production adapter can be wired up.
type DNSResolver interface {
// Resolve performs edge discovery for the given region string (empty for
// global, "us" / "fed" for regional endpoints) and returns the resolved
// addresses grouped by CNAME target, mirroring the structure returned by
// allregions.EdgeDiscovery.
Resolve(region string) ([][]*allregions.EdgeAddr, error)
}
// TCPDialer abstracts the TCP + TLS handshake used by HTTP/2 connectivity probes.
//
// The production implementation wraps edgediscovery.DialEdge
// (edgediscovery/dial.go), which is the same function supervisor/tunnel.go
// uses for production HTTP/2 connections. Reusing it ensures the pre-check
// validates the identical dial path the tunnel will take.
type TCPDialer interface {
// DialEdge dials the given edge TCP address with TLS, respecting the
// provided timeout, and returns the established connection. The caller is
// responsible for closing the connection.
DialEdge(ctx context.Context, timeout time.Duration, tlsConfig *tls.Config, addr *net.TCPAddr, localIP net.IP) (net.Conn, error)
}
// QUICDialer abstracts the UDP + QUIC handshake used by QUIC connectivity probes.
//
// The production implementation wraps connection.DialQuic
// (connection/quic.go), which is the same function supervisor/tunnel.go uses
// for production QUIC connections. The pre-check performs a handshake only —
// no streams are opened and no RPC frames are sent — to avoid triggering the
// OTD registration timeout described in TUN-6732.
type QUICDialer interface {
// DialQuic performs a QUIC handshake to the given edge address and returns
// the established connection. The caller is responsible for closing the
// connection. connIndex is used for UDP port reuse bookkeeping consistent
// with the production dial path.
DialQuic(
ctx context.Context,
quicConfig *quic.Config,
tlsConfig *tls.Config,
addr netip.AddrPort,
localAddr net.IP,
connIndex uint8,
logger *zerolog.Logger,
) (quic.Connection, error)
}
// ManagementDialer abstracts the TCP dial to api.cloudflare.com:443 used by
// the Management API probe.
//
// A successful TCP connection (no TLS handshake required) is sufficient to
// confirm that port 443 is reachable. This probe is always a soft failure:
// the tunnel can run without it, but automatic software updates will be
// unavailable.
type ManagementDialer interface {
// DialContext opens a TCP connection to the given network address. The
// caller is responsible for closing the connection.
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
}
+218
View File
@@ -0,0 +1,218 @@
package prechecks
import (
"bytes"
"fmt"
"strings"
"text/tabwriter"
"github.com/rs/zerolog"
)
const (
// tableWidth is the total character width of the separator lines.
tableWidth = 80
// Status names.
passStatus = "PASS"
failStatus = "FAIL"
skipStatus = "SKIP"
unknownStatus = "UNKNOWN"
// Section separators.
sectionChar = "-"
headerTitle = "CONNECTIVITY PRE-CHECKS"
// Log message constants.
logMsgPrecheck = "precheck"
logMsgPrecheckComplete = "precheck complete"
// Log field names.
logFieldRunID = "run_id"
logFieldComponent = "component"
logFieldTarget = "target"
logFieldStatus = "status"
logFieldDetails = "details"
logFieldHardFail = "hard_fail"
logFieldSuggestedProtocol = "suggested_protocol"
sep = " "
)
// statusLabel returns the display label for a given Status.
func (s Status) statusLabel() string {
switch s {
case Pass:
return passStatus
case Fail:
return failStatus
case Skip:
return skipStatus
default:
return unknownStatus
}
}
// logString returns the lowercase string used in structured log fields.
func (s Status) logString() string {
return strings.ToLower(s.String())
}
// separator returns a full-width horizontal line.
func separator() string {
return strings.Repeat(sectionChar, tableWidth)
}
// header returns the top section title line.
func header() string {
leftDashes := strings.Repeat(sectionChar, 3)
rightLen := tableWidth - len(leftDashes) - len(headerTitle) - len(sep)*2
return leftDashes + sep + headerTitle + sep + strings.Repeat(sectionChar, rightLen)
}
// renderTable uses text/tabwriter to format the results rows with
// automatically aligned columns, returning the rendered string.
func renderTable(results []CheckResult) string {
var buf bytes.Buffer
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "COMPONENT\tTARGET\tSTATUS\tDETAILS")
for _, r := range results {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
}
_ = w.Flush()
return buf.String()
}
// renderActions collects all non-empty Action strings from results and returns
// the formatted warning/error block that appears between the table and SUMMARY.
// A Fail result is rendered as ERROR when the report is a hard fail, and as
// WARNING otherwise (degraded but tunnel can still run).
func renderActions(r Report) string {
hardFail := r.hasHardFail()
var sb strings.Builder
for _, res := range r.Results {
if res.Action == "" || res.ProbeStatus != Fail {
continue
}
if hardFail {
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
} else {
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
}
}
return sb.String()
}
// summaryLine builds the SUMMARY: line based on the Report state.
func summaryLine(r Report) string {
switch {
case r.hasHardFail():
return "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
case r.hasWarn():
return fmt.Sprintf("SUMMARY: Environment ready with degraded transport. cloudflared will proceed using '%s'.", r.SuggestedProtocol)
default:
return fmt.Sprintf("SUMMARY: Environment is healthy. cloudflared will use '%s' as primary protocol.", r.SuggestedProtocol)
}
}
// hasHardFail returns true when the environment cannot establish a tunnel:
// - Any DNS probe failed, OR
// - Both the QUIC and HTTP/2 transport probes failed.
//
// A single transport failing is not a hard fail — the other transport can be
// used as a fallback (degraded state, reported via hasWarn).
func (r Report) hasHardFail() bool {
var quicFail, http2Fail bool
for _, res := range r.Results {
if res.ProbeStatus != Fail {
continue
}
switch res.Type {
case ProbeTypeDNS:
return true
case ProbeTypeQUIC:
quicFail = true
case ProbeTypeHTTP2:
http2Fail = true
case ProbeTypeManagementAPI:
// Management API failure is not a hard fail
}
}
return quicFail && http2Fail
}
// hasWarn returns true when the environment is degraded but functional:
// - Exactly one transport (QUIC or HTTP/2) failed, OR
// - The Management API is unreachable (auto-updates unavailable).
//
// Hard-fail conditions (DNS down, both transports blocked) take precedence
// and will cause hasWarn to return false.
func (r Report) hasWarn() bool {
if r.hasHardFail() {
return false
}
var quicFail, http2Fail, apiFail bool
for _, res := range r.Results {
if res.ProbeStatus != Fail {
continue
}
switch res.Type {
case ProbeTypeQUIC:
quicFail = true
case ProbeTypeHTTP2:
http2Fail = true
case ProbeTypeManagementAPI:
apiFail = true
case ProbeTypeDNS:
// DNS failures are only relevant for hard failures
}
}
return (quicFail != http2Fail) || apiFail
}
// String renders the Report as a human-readable table suitable for os.Stdout.
func (r Report) String() string {
var sb strings.Builder
sb.WriteString(header())
sb.WriteString("\n")
sb.WriteString(renderTable(r.Results))
actions := renderActions(r)
if actions != "" {
sb.WriteString(actions)
}
sb.WriteString("\n")
sb.WriteString(summaryLine(r))
sb.WriteString("\n")
sb.WriteString(separator())
sb.WriteString("\n")
return sb.String()
}
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
// a final summary event. This is the JSON-logging equivalent of String().
// Every line carries run_id so all results from a single invocation can be correlated.
func (r Report) LogEvent(logger *zerolog.Logger) {
runID := r.RunID.String()
for _, res := range r.Results {
logger.Info().
Str(logFieldRunID, runID).
Str(logFieldComponent, res.Component).
Str(logFieldTarget, res.Target).
Str(logFieldStatus, res.ProbeStatus.logString()).
Str(logFieldDetails, res.Details).
Msg(logMsgPrecheck)
}
logger.Info().
Str(logFieldRunID, runID).
Bool(logFieldHardFail, r.hasHardFail()).
Str(logFieldSuggestedProtocol, r.SuggestedProtocol.String()).
Msg(logMsgPrecheckComplete)
}
+418
View File
@@ -0,0 +1,418 @@
package prechecks
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/connection"
)
// fixedRunID is used in all fixtures so golden strings are deterministic.
var fixedRunID = uuid.MustParse("00000000-0000-0000-0000-000000000001")
// Fixtures
// allPassReport is the all-checks-pass scenario: QUIC is the suggested protocol.
func allPassReport() Report {
return Report{
RunID: fixedRunID,
SuggestedProtocol: connection.QUIC,
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: "Handshake successful"},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
},
}
}
// quicBlockedReport is the degraded scenario: QUIC is blocked, HTTP/2 is the fallback.
// Only one transport failed so this is a warning, not a hard fail.
func quicBlockedReport() Report {
return Report{
RunID: fixedRunID,
SuggestedProtocol: connection.HTTP2,
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{
Type: ProbeTypeQUIC,
Component: "UDP Connectivity",
Target: "Port 7844 (QUIC)",
ProbeStatus: Fail,
Details: "Handshake failed",
Action: "Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.",
},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
},
}
}
// apiFailReport is the degraded scenario: all connectivity passes but the Cloudflare
// API is unreachable. The tunnel can still run; only automatic updates are unavailable.
func apiFailReport() Report {
return Report{
RunID: fixedRunID,
SuggestedProtocol: connection.QUIC,
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: "Handshake successful"},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
{
Type: ProbeTypeManagementAPI,
Component: "Cloudflare API",
Target: "api.cloudflare.com:443",
ProbeStatus: Fail,
Details: "Connection refused",
Action: "cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.",
},
},
}
}
// bothTransportsBlockedReport is the hard-fail scenario: both QUIC and HTTP/2 are blocked.
func bothTransportsBlockedReport() Report {
return Report{
RunID: fixedRunID,
SuggestedProtocol: connection.HTTP2,
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{
Type: ProbeTypeQUIC,
Component: "UDP Connectivity",
Target: "Port 7844 (QUIC)",
ProbeStatus: Fail,
Details: "Handshake failed",
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
},
{
Type: ProbeTypeHTTP2,
Component: "TCP Connectivity",
Target: "Port 7844 (HTTP/2)",
ProbeStatus: Fail,
Details: "Blocked or unreachable",
},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
},
}
}
// dnsFailReport is the hard-fail scenario: DNS is unresolvable, transports are skipped.
func dnsFailReport() Report {
return Report{
RunID: fixedRunID,
SuggestedProtocol: connection.HTTP2,
Results: []CheckResult{
{
Type: ProbeTypeDNS,
Component: "DNS Resolution",
Target: "region1.v2.argotunnel.com",
ProbeStatus: Fail,
Details: "No addresses returned",
Action: "Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.",
},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Fail, Details: "No addresses returned"},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Skip, Details: "DNS prerequisite failed"},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Skip, Details: "DNS prerequisite failed"},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Fail, Details: "Connection refused"},
},
}
}
// String() / table renderer tests
func TestString_AllPass(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
"\n" +
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" +
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, allPassReport().String())
}
func TestString_QuicBlocked(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
"WARNING: Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.\n" +
"\n" +
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" +
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, quicBlockedReport().String())
}
func TestString_APIFail(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
"WARNING: cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.\n" +
"\n" +
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.\n" +
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, apiFailReport().String())
}
func TestString_BothTransportsBlocked(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) FAIL Blocked or unreachable\n" +
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.\n" +
"\n" +
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, bothTransportsBlockedReport().String())
}
func TestString_DNSFail(t *testing.T) {
t.Parallel()
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned\n" +
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned\n" +
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed\n" +
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.\n" +
"\n" +
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
"--------------------------------------------------------------------------------\n"
assert.Equal(t, want, dnsFailReport().String())
}
func TestString_EmptyResults(t *testing.T) {
t.Parallel()
r := Report{RunID: fixedRunID, SuggestedProtocol: connection.QUIC}
out := r.String()
// Must not panic and must still emit a valid skeleton.
assert.Contains(t, out, "CONNECTIVITY PRE-CHECKS")
assert.Contains(t, out, "SUMMARY:")
assert.Contains(t, out, separator())
}
// LogEvent() / structured log renderer tests
// logEntry is a helper struct to unmarshal a single JSON log line emitted by LogEvent.
type logEntry struct {
Level string `json:"level"`
RunID string `json:"run_id"`
Component string `json:"component"`
Target string `json:"target"`
Status string `json:"status"`
Details string `json:"details"`
Message string `json:"message"`
HardFail *bool `json:"hard_fail"`
SuggestedProtocol string `json:"suggested_protocol"`
}
// captureLogLines runs LogEvent against a buffer-backed zerolog logger and
// returns the parsed JSON entries, one per emitted line.
func captureLogLines(t *testing.T, r Report) []logEntry {
t.Helper()
var buf bytes.Buffer
logger := zerolog.New(&buf)
r.LogEvent(&logger)
var entries []logEntry
for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") {
if line == "" {
continue
}
var e logEntry
require.NoError(t, json.Unmarshal([]byte(line), &e), "failed to parse log line: %s", line)
entries = append(entries, e)
}
return entries
}
func TestLogEvent_AllPass(t *testing.T) {
t.Parallel()
r := allPassReport()
entries := captureLogLines(t, r)
// One line per result plus one summary line.
require.Len(t, entries, len(r.Results)+1)
// Each result line carries the right fields.
expected := []struct {
component string
target string
status string
details string
}{
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", "Resolved successfully"},
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", "Resolved successfully"},
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", "Handshake successful"},
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", "TLS handshake successful"},
{"Cloudflare API", "api.cloudflare.com:443", "pass", "Reachable"},
}
for i, exp := range expected {
e := entries[i]
assert.Equal(t, "info", e.Level, "entry %d: level", i)
assert.Equal(t, fixedRunID.String(), e.RunID, "entry %d: run_id", i)
assert.Equal(t, exp.component, e.Component, "entry %d: component", i)
assert.Equal(t, exp.target, e.Target, "entry %d: target", i)
assert.Equal(t, exp.status, e.Status, "entry %d: status", i)
assert.Equal(t, exp.details, e.Details, "entry %d: details", i)
assert.Equal(t, logMsgPrecheck, e.Message, "entry %d: message", i)
}
// Summary line.
summary := entries[len(entries)-1]
assert.Equal(t, logMsgPrecheckComplete, summary.Message)
assert.Equal(t, fixedRunID.String(), summary.RunID)
require.NotNil(t, summary.HardFail)
assert.False(t, *summary.HardFail)
assert.Equal(t, "quic", summary.SuggestedProtocol)
}
func TestLogEvent_QuicBlocked(t *testing.T) {
t.Parallel()
entries := captureLogLines(t, quicBlockedReport())
// QUIC row (index 2) must carry status=fail and the right details.
quic := entries[2]
assert.Equal(t, "fail", quic.Status)
assert.Equal(t, "UDP Connectivity", quic.Component)
assert.Equal(t, "Port 7844 (QUIC)", quic.Target)
assert.Equal(t, "Handshake failed", quic.Details)
assert.Equal(t, fixedRunID.String(), quic.RunID)
// Summary: not a hard fail (HTTP/2 still works), protocol falls back to http2.
summary := entries[len(entries)-1]
require.NotNil(t, summary.HardFail)
assert.False(t, *summary.HardFail)
assert.Equal(t, "http2", summary.SuggestedProtocol)
assert.Equal(t, fixedRunID.String(), summary.RunID)
}
func TestLogEvent_APIFail(t *testing.T) {
t.Parallel()
entries := captureLogLines(t, apiFailReport())
// API row (index 4) carries status=fail and the expected details.
api := entries[4]
assert.Equal(t, "fail", api.Status)
assert.Equal(t, "Cloudflare API", api.Component)
assert.Equal(t, "api.cloudflare.com:443", api.Target)
assert.Equal(t, "Connection refused", api.Details)
assert.Equal(t, fixedRunID.String(), api.RunID)
// All transport rows pass.
assert.Equal(t, "pass", entries[2].Status)
assert.Equal(t, "pass", entries[3].Status)
// Summary: not a hard fail, QUIC is still the suggested protocol.
summary := entries[len(entries)-1]
require.NotNil(t, summary.HardFail)
assert.False(t, *summary.HardFail)
assert.Equal(t, "quic", summary.SuggestedProtocol)
}
func TestLogEvent_BothTransportsBlocked(t *testing.T) {
t.Parallel()
entries := captureLogLines(t, bothTransportsBlockedReport())
// Both transport rows carry status=fail.
assert.Equal(t, "fail", entries[2].Status)
assert.Equal(t, "Handshake failed", entries[2].Details)
assert.Equal(t, "fail", entries[3].Status)
assert.Equal(t, "Blocked or unreachable", entries[3].Details)
// Summary: hard fail is true.
summary := entries[len(entries)-1]
require.NotNil(t, summary.HardFail)
assert.True(t, *summary.HardFail)
}
func TestLogEvent_DNSFail(t *testing.T) {
t.Parallel()
entries := captureLogLines(t, dnsFailReport())
// Both DNS rows carry status=fail and the expected details.
assert.Equal(t, "fail", entries[0].Status)
assert.Equal(t, "No addresses returned", entries[0].Details)
assert.Equal(t, "fail", entries[1].Status)
assert.Equal(t, "No addresses returned", entries[1].Details)
// Transport rows are skipped.
assert.Equal(t, "skip", entries[2].Status)
assert.Equal(t, "DNS prerequisite failed", entries[2].Details)
assert.Equal(t, "skip", entries[3].Status)
assert.Equal(t, "DNS prerequisite failed", entries[3].Details)
// Summary: hard fail is true.
summary := entries[len(entries)-1]
require.NotNil(t, summary.HardFail)
assert.True(t, *summary.HardFail)
}
func TestLogEvent_EmptyReport(t *testing.T) {
t.Parallel()
r := Report{RunID: fixedRunID, SuggestedProtocol: connection.HTTP2}
entries := captureLogLines(t, r)
// No result lines, only the summary.
require.Len(t, entries, 1)
assert.Equal(t, logMsgPrecheckComplete, entries[0].Message)
assert.Equal(t, fixedRunID.String(), entries[0].RunID)
require.NotNil(t, entries[0].HardFail)
assert.False(t, *entries[0].HardFail)
assert.Equal(t, "http2", entries[0].SuggestedProtocol)
}
// hasHardFail / hasWarn helper tests
func TestHasHardFail(t *testing.T) {
t.Parallel()
assert.False(t, allPassReport().hasHardFail())
assert.False(t, quicBlockedReport().hasHardFail())
assert.False(t, apiFailReport().hasHardFail())
assert.True(t, bothTransportsBlockedReport().hasHardFail())
assert.True(t, dnsFailReport().hasHardFail())
}
func TestHasWarn(t *testing.T) {
t.Parallel()
assert.False(t, allPassReport().hasWarn())
assert.True(t, quicBlockedReport().hasWarn())
assert.True(t, apiFailReport().hasWarn())
assert.False(t, bothTransportsBlockedReport().hasWarn())
assert.False(t, dnsFailReport().hasWarn())
}
+111
View File
@@ -0,0 +1,111 @@
package prechecks
import (
"time"
"github.com/google/uuid"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
)
// Status represents the outcome of a single connectivity pre-check.
type Status int
const (
// Pass indicates the check completed successfully.
Pass Status = iota
// Fail indicates the check did not succeed. Whether this is a hard failure
// or a degraded-but-functional state depends on which probe(s) failed — see
// Report.hasHardFail and Report.hasWarn.
Fail
// Skip indicates the check was not executed because a prerequisite
// check (typically DNS) failed first.
Skip
)
// String returns the canonical display name for a Status value.
func (s Status) String() string {
switch s {
case Pass:
return "PASS"
case Fail:
return "FAIL"
case Skip:
return "SKIP"
default:
return "UNKNOWN"
}
}
// ProbeType identifies which connectivity probe produced a CheckResult.
// It is used by hasHardFail and hasWarn to evaluate severity without
// matching against human-readable strings.
type ProbeType int
const (
ProbeTypeDNS ProbeType = iota // DNS resolution
ProbeTypeQUIC // UDP/QUIC transport
ProbeTypeHTTP2 // TCP/HTTP2 transport
ProbeTypeManagementAPI // Cloudflare management API
)
// CheckResult holds the outcome of one individual connectivity probe.
type CheckResult struct {
// Type identifies which probe produced this result. Used for severity
// classification in hasHardFail and hasWarn.
Type ProbeType
// Component is the human-readable probe category shown in the table header
// column, e.g. "DNS Resolution", "QUIC Connectivity".
Component string
// Target is the address or resource that was probed, e.g.
// "region1.v2.argotunnel.com" or "Port 7844 (QUIC)".
Target string
// ProbeStatus is the outcome of the probe.
ProbeStatus Status
// Details is a short description of the result shown in the table, e.g.
// "Resolved successfully" or "Handshake failed".
Details string
// Action is non-empty when ProbeStatus is Fail and contains a human-readable
// remediation instruction, e.g. "Allow outbound QUIC on port 7844."
Action string
}
// Report aggregates all CheckResults produced by a single Run() invocation.
// Pre-checks run in parallel with tunnel initialization and are purely
// diagnostic: the Report is displayed to the user but never gates startup.
type Report struct {
// RunID is a unique identifier for this pre-check run. It is included in
// every structured log line so that all results from a single invocation
// can be correlated across log aggregation systems.
RunID uuid.UUID
// Results contains one entry per executed probe, in the order they were
// collected.
Results []CheckResult
// SuggestedProtocol is the connection protocol the pre-checks recommend
// based on transport probe results.
SuggestedProtocol connection.Protocol
}
// Config controls the behavior of a pre-check Run().
type Config struct {
// Region is the optional cloudflared --region flag value. When non-empty
// the pre-check probes the regional edge hostnames instead of the global ones.
Region string
// Timeout is the maximum wall-clock duration allowed for the entire
// pre-check suite to complete.
Timeout time.Duration
// IPVersion controls which address families are probed for transport
// checks. It mirrors the --edge-ip-version CLI flag so that the pre-check
// exercises the same code paths the tunnel itself will use.
IPVersion allregions.ConfigIPVersion
}
+125 -4
View File
@@ -186,20 +186,141 @@ sudo dnf install cloudflared
</pre>
<h2><a name="gokeyless-packages"></a>Gokeyless</a></h2>
<h3><a name="go-keyless-debian">Debian</a></h3>
<h2><a name="gokeyless-packages">Gokeyless</a></h2>
<ul>
<li><a href="#gokeyless-debian-any">Any Debian Based Distribution (Recommended)</a></li>
<li><a href="#gokeyless-debian-bookworm">Debian Bookworm</a></li>
<li><a href="#gokeyless-ubuntu-focal">Ubuntu 20.04 (Focal Fossa)</a></li>
<li><a href="#gokeyless-ubuntu-jammy">Ubuntu 22.04 (Jammy Jellyfish)</a></li>
<li><a href="#gokeyless-ubuntu-noble">Ubuntu 24.04 (Noble Numbat)</a></li>
<li><a href="#gokeyless-amazon-linux">Amazon Linux</a></li>
<li><a href="#gokeyless-rhel-generic">RHEL Generic</a></li>
<li><a href="#gokeyless-centos-7">Centos 7</a></li>
<li><a href="#gokeyless-centos-8">Centos 8</a></li>
<li><a href="#gokeyless-centos-stream">Centos Stream</a></li>
</ul>
<h3 style="color: #d9534f;">Warning: Public Key Rollover (30 October 2025)</h3>
<p style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 10px; margin: 10px 0;">
<strong>We have rolled our public key for package signing.</strong> If you are using RPM-based distributions (RHEL,
CentOS, Amazon Linux, etc.) or Debian Trixie and have the old key installed, RPM/Deb packages will no longer work with the old key.
Please update your repository configuration using the instructions below to ensure you can continue receiving
package updates. The previous keys will still work for other distributions for the time being, but it is now DEPRECATED and will be removed on 30 April 2026
</p>
<h3><a name="gokeyless-debian-any">Any Debian Based Distribution (Recommended)</a></h3>
<pre>
# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless buster main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless any main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
# install gokeyless
sudo apt-get update && sudo apt-get install gokeyless
</pre>
<h3><a name="go-keyless-centos">Centos 8</a></h3>
<h3><a name="gokeyless-debian-bookworm">Debian Bookworm</a></h3>
<pre>
# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless bookworm main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
# install gokeyless
sudo apt-get update && sudo apt-get install gokeyless
</pre>
<h3><a name="gokeyless-ubuntu-focal">Ubuntu 20.04 (Focal Fossa)</a></h3>
<pre>
# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless focal main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
# install gokeyless
sudo apt-get update && sudo apt-get install gokeyless
</pre>
<h3><a name="gokeyless-ubuntu-jammy">Ubuntu 22.04 (Jammy Jellyfish)</a></h3>
<pre>
# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless jammy main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
# install gokeyless
sudo apt-get update && sudo apt-get install gokeyless
</pre>
<h3><a name="gokeyless-ubuntu-noble">Ubuntu 24.04 (Noble Numbat)</a></h3>
<pre>
# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/gokeyless noble main' | sudo tee /etc/apt/sources.list.d/cloudflare.list
# install gokeyless
sudo apt-get update && sudo apt-get install gokeyless
</pre>
<h3><a name="gokeyless-amazon-linux">Amazon Linux</a></h3>
<pre>
# Add gokeyless.repo to /etc/yum.repos.d/
curl -fsSl https://pkg.cloudflare.com/gokeyless.repo | sudo tee /etc/yum.repos.d/gokeyless.repo
#update repo
sudo yum update
# install gokeyless
sudo yum install gokeyless
</pre>
<h3><a name="gokeyless-rhel-generic">RHEL Generic</a></h3>
<pre>
# Add gokeyless.repo to /etc/yum.repos.d/
curl -fsSl https://pkg.cloudflare.com/gokeyless.repo | sudo tee /etc/yum.repos.d/gokeyless.repo
#update repo
sudo yum update
# install gokeyless
sudo yum install gokeyless
</pre>
<h3><a name="gokeyless-centos-7">Centos 7</a></h3>
<pre>
# This requires yum config-manager
sudo yum install yum-utils
# Add gokeyless.repo to config-manager
sudo yum-config-manager --add-repo https://pkg.cloudflare.com/gokeyless.repo
# install gokeyless
sudo yum install gokeyless
</pre>
<h3><a name="gokeyless-centos-8">Centos 8</a></h3>
<pre>
# This requires dnf config-manager
# Add gokeyless.repo to config-manager
sudo dnf config-manager --add-repo https://pkg.cloudflare.com/gokeyless.repo
# install gokeyless
sudo dnf install gokeyless
</pre>
<h3><a name="gokeyless-centos-stream">Centos Stream</a></h3>
<pre>
# This requires dnf config-manager
# Add gokeyless.repo to config-manager
-39
View File
@@ -1,39 +0,0 @@
package tunneldns
import (
"context"
"github.com/coredns/coredns/plugin"
"github.com/miekg/dns"
"github.com/pkg/errors"
)
// Upstream is a simplified interface for proxy destination
type Upstream interface {
Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error)
}
// ProxyPlugin is a simplified DNS proxy using a generic upstream interface
type ProxyPlugin struct {
Upstreams []Upstream
Next plugin.Handler
}
// ServeDNS implements interface for CoreDNS plugin
func (p ProxyPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
var reply *dns.Msg
var backendErr error
for _, upstream := range p.Upstreams {
reply, backendErr = upstream.Exchange(ctx, r)
if backendErr == nil {
w.WriteMsg(reply)
return 0, nil
}
}
return dns.RcodeServerFailure, errors.Wrap(backendErr, "failed to contact any of the upstreams")
}
// Name implements interface for CoreDNS plugin
func (p ProxyPlugin) Name() string { return "proxy" }
-145
View File
@@ -1,145 +0,0 @@
package tunneldns
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/miekg/dns"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"golang.org/x/net/http2"
)
const (
defaultTimeout = 5 * time.Second
)
// UpstreamHTTPS is the upstream implementation for DNS over HTTPS service
type UpstreamHTTPS struct {
client *http.Client
endpoint *url.URL
bootstraps []string
log *zerolog.Logger
}
// NewUpstreamHTTPS creates a new DNS over HTTPS upstream from endpoint
func NewUpstreamHTTPS(endpoint string, bootstraps []string, maxConnections int, log *zerolog.Logger) (Upstream, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
return &UpstreamHTTPS{client: configureClient(u.Hostname(), maxConnections), endpoint: u, bootstraps: bootstraps, log: log}, nil
}
// Exchange provides an implementation for the Upstream interface
func (u *UpstreamHTTPS) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error) {
queryBuf, err := query.Pack()
if err != nil {
return nil, errors.Wrap(err, "failed to pack DNS query")
}
if len(query.Question) > 0 && query.Question[0].Name == fmt.Sprintf("%s.", u.endpoint.Hostname()) {
for _, bootstrap := range u.bootstraps {
endpoint, client, err := configureBootstrap(bootstrap)
if err != nil {
u.log.Err(err).Msgf("failed to configure bootstrap upstream %s", bootstrap)
continue
}
msg, err := exchange(queryBuf, query.Id, endpoint, client, u.log)
if err != nil {
u.log.Err(err).Msgf("failed to connect to a bootstrap upstream %s", bootstrap)
continue
}
return msg, nil
}
return nil, fmt.Errorf("failed to reach any bootstrap upstream: %v", u.bootstraps)
}
return exchange(queryBuf, query.Id, u.endpoint, u.client, u.log)
}
func exchange(msg []byte, queryID uint16, endpoint *url.URL, client *http.Client, log *zerolog.Logger) (*dns.Msg, error) {
// No content negotiation for now, use DNS wire format
buf, backendErr := exchangeWireformat(msg, endpoint, client)
if backendErr == nil {
response := &dns.Msg{}
if err := response.Unpack(buf); err != nil {
return nil, errors.Wrap(err, "failed to unpack DNS response from body")
}
response.Id = queryID
return response, nil
}
log.Err(backendErr).Msgf("failed to connect to an HTTPS backend %q", endpoint)
return nil, backendErr
}
// Perform message exchange with the default UDP wireformat defined in current draft
// https://datatracker.ietf.org/doc/draft-ietf-doh-dns-over-https
func exchangeWireformat(msg []byte, endpoint *url.URL, client *http.Client) ([]byte, error) {
req, err := http.NewRequest("POST", endpoint.String(), bytes.NewBuffer(msg))
if err != nil {
return nil, errors.Wrap(err, "failed to create an HTTPS request")
}
req.Header.Add("Content-Type", "application/dns-message")
req.Host = endpoint.Host
resp, err := client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "failed to perform an HTTPS request")
}
// Check response status code
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("returned status code %d", resp.StatusCode)
}
// Read wireformat response from the body
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read the response body")
}
return buf, nil
}
func configureBootstrap(bootstrap string) (*url.URL, *http.Client, error) {
b, err := url.Parse(bootstrap)
if err != nil {
return nil, nil, err
}
if ip := net.ParseIP(b.Hostname()); ip == nil {
return nil, nil, fmt.Errorf("bootstrap address of %s must be an IP address", b.Hostname())
}
return b, configureClient(b.Hostname(), MaxUpstreamConnsDefault), nil
}
// configureClient will configure a HTTPS client for upstream DoH requests
func configureClient(hostname string, maxUpstreamConnections int) *http.Client {
// Update TLS and HTTP client configuration
tlsConfig := &tls.Config{ServerName: hostname}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
DisableCompression: true,
MaxIdleConns: 1,
MaxConnsPerHost: maxUpstreamConnections,
Proxy: http.ProxyFromEnvironment,
}
_ = http2.ConfigureTransport(transport)
return &http.Client{
Timeout: defaultTimeout,
Transport: transport,
}
}
-44
View File
@@ -1,44 +0,0 @@
package tunneldns
import (
"context"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/metrics/vars"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/rcode"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
const (
pluginName = "cloudflared"
)
// MetricsPlugin is an adapter for CoreDNS and built-in metrics
type MetricsPlugin struct {
Next plugin.Handler
}
// NewMetricsPlugin creates a plugin with configured metrics
func NewMetricsPlugin(next plugin.Handler) *MetricsPlugin {
return &MetricsPlugin{Next: next}
}
// ServeDNS implements the CoreDNS plugin interface
func (p MetricsPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
rw := dnstest.NewRecorder(w)
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
// Update built-in metrics
server := metrics.WithServer(ctx)
vars.Report(server, state, ".", "", rcode.ToString(rw.Rcode), pluginName, rw.Len, rw.Start)
return status, err
}
// Name implements the CoreDNS plugin interface
func (p MetricsPlugin) Name() string { return "metrics" }
-109
View File
@@ -1,109 +0,0 @@
package tunneldns
import (
"net"
"strconv"
"sync"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/cache"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
const (
LogFieldAddress = "address"
LogFieldURL = "url"
MaxUpstreamConnsDefault = 5
)
// Listener is an adapter between CoreDNS server and Warp runnable
type Listener struct {
server *dnsserver.Server
wg sync.WaitGroup
log *zerolog.Logger
}
// Create a CoreDNS server plugin from configuration
func createConfig(address string, port uint16, p plugin.Handler) *dnsserver.Config {
c := &dnsserver.Config{
Zone: ".",
Transport: "dns",
ListenHosts: []string{address},
Port: strconv.FormatUint(uint64(port), 10),
}
c.AddPlugin(func(next plugin.Handler) plugin.Handler { return p })
return c
}
// Start blocks for serving requests
func (l *Listener) Start(readySignal chan struct{}) error {
defer close(readySignal)
l.log.Info().Str(LogFieldAddress, l.server.Address()).Msg("Starting DNS over HTTPS proxy server")
// Start UDP listener
if udp, err := l.server.ListenPacket(); err == nil {
l.wg.Add(1)
go func() {
_ = l.server.ServePacket(udp)
l.wg.Done()
}()
} else {
return errors.Wrap(err, "failed to create a UDP listener")
}
// Start TCP listener
tcp, err := l.server.Listen()
if err == nil {
l.wg.Add(1)
go func() {
_ = l.server.Serve(tcp)
l.wg.Done()
}()
}
return errors.Wrap(err, "failed to create a TCP listener")
}
// Stop signals server shutdown and blocks until completed
func (l *Listener) Stop() error {
if err := l.server.Stop(); err != nil {
return err
}
l.wg.Wait()
return nil
}
// CreateListener configures the server and bound sockets
func CreateListener(address string, port uint16, upstreams []string, bootstraps []string, maxUpstreamConnections int, log *zerolog.Logger) (*Listener, error) {
// Build the list of upstreams
upstreamList := make([]Upstream, 0)
for _, url := range upstreams {
log.Info().Str(LogFieldURL, url).Msg("Adding DNS upstream")
upstream, err := NewUpstreamHTTPS(url, bootstraps, maxUpstreamConnections, log)
if err != nil {
return nil, errors.Wrap(err, "failed to create HTTPS upstream")
}
upstreamList = append(upstreamList, upstream)
}
// Create a local cache with HTTPS proxy plugin
chain := cache.New()
chain.Next = ProxyPlugin{
Upstreams: upstreamList,
}
// Format an endpoint
endpoint := "dns://" + net.JoinHostPort(address, strconv.FormatUint(uint64(port), 10))
// Create the actual middleware server
server, err := dnsserver.NewServer(endpoint, []*dnsserver.Config{createConfig(address, port, NewMetricsPlugin(chain))})
if err != nil {
return nil, err
}
return &Listener{server: server, log: log}, nil
}
-19
View File
@@ -1,19 +0,0 @@
Copyright (c) 2015 Martin Atkins
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-236
View File
@@ -1,236 +0,0 @@
// Package cidr is a collection of assorted utilities for computing
// network and host addresses within network ranges.
//
// It expects a CIDR-type address structure where addresses are divided into
// some number of prefix bits representing the network and then the remaining
// suffix bits represent the host.
//
// For example, it can help to calculate addresses for sub-networks of a
// parent network, or to calculate host addresses within a particular prefix.
//
// At present this package is prioritizing simplicity of implementation and
// de-prioritizing speed and memory usage. Thus caution is advised before
// using this package in performance-critical applications or hot codepaths.
// Patches to improve the speed and memory usage may be accepted as long as
// they do not result in a significant increase in code complexity.
package cidr
import (
"fmt"
"math/big"
"net"
)
// Subnet takes a parent CIDR range and creates a subnet within it
// with the given number of additional prefix bits and the given
// network number.
//
// For example, 10.3.0.0/16, extended by 8 bits, with a network number
// of 5, becomes 10.3.5.0/24 .
func Subnet(base *net.IPNet, newBits int, num int) (*net.IPNet, error) {
return SubnetBig(base, newBits, big.NewInt(int64(num)))
}
// SubnetBig takes a parent CIDR range and creates a subnet within it with the
// given number of additional prefix bits and the given network number. It
// differs from Subnet in that it takes a *big.Int for the num, instead of an int.
//
// For example, 10.3.0.0/16, extended by 8 bits, with a network number of 5,
// becomes 10.3.5.0/24 .
func SubnetBig(base *net.IPNet, newBits int, num *big.Int) (*net.IPNet, error) {
ip := base.IP
mask := base.Mask
parentLen, addrLen := mask.Size()
newPrefixLen := parentLen + newBits
if newPrefixLen > addrLen {
return nil, fmt.Errorf("insufficient address space to extend prefix of %d by %d", parentLen, newBits)
}
maxNetNum := uint64(1<<uint64(newBits)) - 1
if num.Uint64() > maxNetNum {
return nil, fmt.Errorf("prefix extension of %d does not accommodate a subnet numbered %d", newBits, num)
}
return &net.IPNet{
IP: insertNumIntoIP(ip, num, newPrefixLen),
Mask: net.CIDRMask(newPrefixLen, addrLen),
}, nil
}
// Host takes a parent CIDR range and turns it into a host IP address with the
// given host number.
//
// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2.
func Host(base *net.IPNet, num int) (net.IP, error) {
return HostBig(base, big.NewInt(int64(num)))
}
// HostBig takes a parent CIDR range and turns it into a host IP address with
// the given host number. It differs from Host in that it takes a *big.Int for
// the num, instead of an int.
//
// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2.
func HostBig(base *net.IPNet, num *big.Int) (net.IP, error) {
ip := base.IP
mask := base.Mask
parentLen, addrLen := mask.Size()
hostLen := addrLen - parentLen
maxHostNum := big.NewInt(int64(1))
maxHostNum.Lsh(maxHostNum, uint(hostLen))
maxHostNum.Sub(maxHostNum, big.NewInt(1))
numUint64 := big.NewInt(int64(num.Uint64()))
if num.Cmp(big.NewInt(0)) == -1 {
numUint64.Neg(num)
numUint64.Sub(numUint64, big.NewInt(int64(1)))
num.Sub(maxHostNum, numUint64)
}
if numUint64.Cmp(maxHostNum) == 1 {
return nil, fmt.Errorf("prefix of %d does not accommodate a host numbered %d", parentLen, num)
}
var bitlength int
if ip.To4() != nil {
bitlength = 32
} else {
bitlength = 128
}
return insertNumIntoIP(ip, num, bitlength), nil
}
// AddressRange returns the first and last addresses in the given CIDR range.
func AddressRange(network *net.IPNet) (net.IP, net.IP) {
// the first IP is easy
firstIP := network.IP
// the last IP is the network address OR NOT the mask address
prefixLen, bits := network.Mask.Size()
if prefixLen == bits {
// Easy!
// But make sure that our two slices are distinct, since they
// would be in all other cases.
lastIP := make([]byte, len(firstIP))
copy(lastIP, firstIP)
return firstIP, lastIP
}
firstIPInt, bits := ipToInt(firstIP)
hostLen := uint(bits) - uint(prefixLen)
lastIPInt := big.NewInt(1)
lastIPInt.Lsh(lastIPInt, hostLen)
lastIPInt.Sub(lastIPInt, big.NewInt(1))
lastIPInt.Or(lastIPInt, firstIPInt)
return firstIP, intToIP(lastIPInt, bits)
}
// AddressCount returns the number of distinct host addresses within the given
// CIDR range.
//
// Since the result is a uint64, this function returns meaningful information
// only for IPv4 ranges and IPv6 ranges with a prefix size of at least 65.
func AddressCount(network *net.IPNet) uint64 {
prefixLen, bits := network.Mask.Size()
return 1 << (uint64(bits) - uint64(prefixLen))
}
//VerifyNoOverlap takes a list subnets and supernet (CIDRBlock) and verifies
//none of the subnets overlap and all subnets are in the supernet
//it returns an error if any of those conditions are not satisfied
func VerifyNoOverlap(subnets []*net.IPNet, CIDRBlock *net.IPNet) error {
firstLastIP := make([][]net.IP, len(subnets))
for i, s := range subnets {
first, last := AddressRange(s)
firstLastIP[i] = []net.IP{first, last}
}
for i, s := range subnets {
if !CIDRBlock.Contains(firstLastIP[i][0]) || !CIDRBlock.Contains(firstLastIP[i][1]) {
return fmt.Errorf("%s does not fully contain %s", CIDRBlock.String(), s.String())
}
for j := 0; j < len(subnets); j++ {
if i == j {
continue
}
first := firstLastIP[j][0]
last := firstLastIP[j][1]
if s.Contains(first) || s.Contains(last) {
return fmt.Errorf("%s overlaps with %s", subnets[j].String(), s.String())
}
}
}
return nil
}
// PreviousSubnet returns the subnet of the desired mask in the IP space
// just lower than the start of IPNet provided. If the IP space rolls over
// then the second return value is true
func PreviousSubnet(network *net.IPNet, prefixLen int) (*net.IPNet, bool) {
startIP := checkIPv4(network.IP)
previousIP := make(net.IP, len(startIP))
copy(previousIP, startIP)
cMask := net.CIDRMask(prefixLen, 8*len(previousIP))
previousIP = Dec(previousIP)
previous := &net.IPNet{IP: previousIP.Mask(cMask), Mask: cMask}
if startIP.Equal(net.IPv4zero) || startIP.Equal(net.IPv6zero) {
return previous, true
}
return previous, false
}
// NextSubnet returns the next available subnet of the desired mask size
// starting for the maximum IP of the offset subnet
// If the IP exceeds the maxium IP then the second return value is true
func NextSubnet(network *net.IPNet, prefixLen int) (*net.IPNet, bool) {
_, currentLast := AddressRange(network)
mask := net.CIDRMask(prefixLen, 8*len(currentLast))
currentSubnet := &net.IPNet{IP: currentLast.Mask(mask), Mask: mask}
_, last := AddressRange(currentSubnet)
last = Inc(last)
next := &net.IPNet{IP: last.Mask(mask), Mask: mask}
if last.Equal(net.IPv4zero) || last.Equal(net.IPv6zero) {
return next, true
}
return next, false
}
//Inc increases the IP by one this returns a new []byte for the IP
func Inc(IP net.IP) net.IP {
IP = checkIPv4(IP)
incIP := make([]byte, len(IP))
copy(incIP, IP)
for j := len(incIP) - 1; j >= 0; j-- {
incIP[j]++
if incIP[j] > 0 {
break
}
}
return incIP
}
//Dec decreases the IP by one this returns a new []byte for the IP
func Dec(IP net.IP) net.IP {
IP = checkIPv4(IP)
decIP := make([]byte, len(IP))
copy(decIP, IP)
decIP = checkIPv4(decIP)
for j := len(decIP) - 1; j >= 0; j-- {
decIP[j]--
if decIP[j] < 255 {
break
}
}
return decIP
}
func checkIPv4(ip net.IP) net.IP {
// Go for some reason allocs IPv6len for IPv4 so we have to correct it
if v4 := ip.To4(); v4 != nil {
return v4
}
return ip
}
-37
View File
@@ -1,37 +0,0 @@
package cidr
import (
"fmt"
"math/big"
"net"
)
func ipToInt(ip net.IP) (*big.Int, int) {
val := &big.Int{}
val.SetBytes([]byte(ip))
if len(ip) == net.IPv4len {
return val, 32
} else if len(ip) == net.IPv6len {
return val, 128
} else {
panic(fmt.Errorf("Unsupported address length %d", len(ip)))
}
}
func intToIP(ipInt *big.Int, bits int) net.IP {
ipBytes := ipInt.Bytes()
ret := make([]byte, bits/8)
// Pack our IP bytes into the end of the return array,
// since big.Int.Bytes() removes front zero padding.
for i := 1; i <= len(ipBytes); i++ {
ret[len(ret)-i] = ipBytes[len(ipBytes)-i]
}
return net.IP(ret)
}
func insertNumIntoIP(ip net.IP, bigNum *big.Int, prefixLen int) net.IP {
ipInt, totalBits := ipToInt(ip)
bigNum.Lsh(bigNum, uint(totalBits-prefixLen))
ipInt.Or(ipInt, bigNum)
return intToIP(ipInt, totalBits)
}
-21
View File
@@ -1,21 +0,0 @@
# shell scripts should not use tabs to indent!
*.bash text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.sh text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# files for systemd (shell-similar)
*.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.timer text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
go.mod text eol=lf
go.sum text eol=lf
*.txt text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.tpl text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.htm text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.html text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.md text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
.git* text eol=auto core.whitespace whitespace=trailing-space
-22
View File
@@ -1,22 +0,0 @@
.DS_Store
Thumbs.db
_gitignore/
Vagrantfile
.vagrant/
/.idea
dist/builds/
dist/release/
error.log
access.log
/*.conf
Caddyfile
!caddyfile/
og_static/
.vscode/
*.bat
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-203
View File
@@ -1,203 +0,0 @@
THIS IS A FORK OF CADDY v1 - EVERYTHING IS STRIPPED EXCEPT THE PIECES NEEDED IN COREDNS.
Issues are not enabled in this repository. Please raise any issues in coredns/coredns.
---
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/caddyserver/caddy/wiki/Running-Caddy-on-Android).
<p align="center">
<b>Thanks to our special sponsor:</b>
<br><br>
<a href="https://relicabackup.com"><img src="https://caddyserver.com/resources/images/sponsors/relica.png" width="220" alt="Relica - Cross-platform file backup to the cloud, local disks, or other computers"></a>
</p>
## Menu
- [Features](#features)
- [Install](#install)
- [Quick Start](#quick-start)
- [Running in Production](#running-in-production)
- [Contributing](#contributing)
- [Donors](#donors)
- [About the Project](#about-the-project)
## Features
- **Easy configuration** with the Caddyfile
- **Automatic HTTPS** on by default (via [Let's Encrypt](https://letsencrypt.org))
- **HTTP/2** by default
- **Virtual hosting** so multiple sites just work
- Experimental **QUIC support** for cutting-edge transmissions
- TLS session ticket **key rotation** for more secure connections
- **Extensible with plugins** because a convenient web server is a helpful one
- **Runs anywhere** with **no external dependencies** (not even libc)
[See a more complete list of features built into Caddy.](https://caddyserver.com/#features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
Altogether, Caddy can do things other web servers simply cannot do. Its features and plugins save you time and mistakes, and will cheer you up. Your Caddy instance takes care of the details for you!
<p align="center">
<b>Powered by</b>
<br>
<a href="https://github.com/mholt/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
</p>
## Install
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
- **[Latest release](https://github.com/caddyserver/caddy/releases/latest)** for pre-built, vanilla binaries
- **[AWS Marketplace](https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C)** makes it easy to deploy directly to your cloud environment. <a href="https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C" target="_blank">
<img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png" alt="Get Caddy on the AWS Marketplace" height="25"/></a>
## Build
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.13 or newer).
**To build Caddy without plugins:**
- Run `go get github.com/caddyserver/caddy/caddy`
Caddy will be installed to your `$GOPATH/bin` folder.
With these instructions, the binary will not have embedded version information (see [golang/go#29228](https://github.com/golang/go/issues/29228)), but it is fine for a quick start.
**To build Caddy with plugins (and with version information):**
There is no need to modify the Caddy code to build it with plugins. We will create a simple Go module with our own `main()` that you can use to make custom Caddy builds.
- Create a new folder anywhere and within create a Go file (with an extension of `.go`, such as `main.go`) with the contents below, adjusting to import the plugins you want to include:
```go
package main
import (
"github.com/caddyserver/caddy/caddy/caddymain"
// plug in plugins here, for example:
// _ "import/path/here"
)
func main() {
// optional: disable telemetry
// caddymain.EnableTelemetry = false
caddymain.Run()
}
```
3. `go mod init caddy`
4. Run `go get github.com/caddyserver/caddy`
5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
**To install Caddy's source code for development:**
- Run `git clone https://github.com/caddyserver/caddy.git` in any folder (doesn't have to be in GOPATH).
You can make changes to the source code from that clone and checkout any commit or tag you wish to develop on.
When building from source, telemetry is enabled by default. You can disable it by changing `caddymain.EnableTelemetry = false` in run.go, or use the `-disabled-metrics` flag at runtime to disable only certain metrics.
## Quick Start
To serve static files from the current working directory, run:
```
caddy
```
Caddy's default port is 2015, so open your browser to [http://localhost:2015](http://localhost:2015).
### Go from 0 to HTTPS in 5 seconds
If the `caddy` binary has permission to bind to low ports and your domain name's DNS records point to the machine you're on:
```
caddy -host example.com
```
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you! Caddy is also automatically configuring ports 80 and 443 for you, and redirecting HTTP to HTTPS. Cool, huh?
### Customizing your site
To customize how your site is served, create a file named Caddyfile by your site and paste this into it:
```plain
localhost
push
browse
websocket /echo cat
ext .html
log /var/log/access.log
proxy /api 127.0.0.1:7005
header /api Access-Control-Allow-Origin *
```
When you run `caddy` in that directory, it will automatically find and use that Caddyfile.
This simple file enables server push (via Link headers), allows directory browsing (for folders without an index file), hosts a WebSocket echo server at /echo, serves clean URLs, logs requests to an access log, proxies all API requests to a backend on port 7005, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from the API.
Wow! Caddy can do a lot with just a few lines.
### Doing more with Caddy
To host multiple sites and do more with the Caddyfile, please see the [Caddyfile tutorial](https://caddyserver.com/tutorial/caddyfile).
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
Caddy has a nice little command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
## Running in Production
Caddy is production-ready if you find it to be a good fit for your site and workflow.
**Running as root:** We advise against this. You can still listen on ports < 1024 on Linux using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. But your download file includes [unofficial resources](https://github.com/caddyserver/caddy/tree/master/dist/init) contributed by the community that you may find helpful for running Caddy in production.
How you choose to run Caddy is up to you. Many users are satisfied with `nohup caddy &`. Others use `screen`. Users who need Caddy to come back up after reboots either do so in the script that caused the reboot, add a command to an init script, or configure a service with their OS.
If you have questions or concerns about Caddy' underlying crypto implementations, consult Go's [crypto packages](https://golang.org/pkg/crypto), starting with their documentation, then issues, then the code itself; as Caddy uses mainly those libraries.
## Contributing
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/)!
Please see our [contributing guidelines](https://github.com/caddyserver/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/caddyserver/caddy/wiki).
We use GitHub issues and pull requests only for discussing bug reports and the development of specific changes. We welcome all other topics on the [forum](https://caddy.community)!
If you want to contribute to the documentation, please [submit an issue](https://github.com/caddyserver/caddy/issues/new) describing the change that should be made.
### Good First Issue
If you are looking for somewhere to start and would like to help out by working on an existing issue, take a look at our [`Good First Issue`](https://github.com/caddyserver/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
Thanks for making Caddy -- and the Web -- better!
## Donors
- [DigitalOcean](https://m.do.co/c/6d7bdafccf96) is hosting the Caddy project.
- [DNSimple](https://dnsimple.link/resolving-caddy) provides DNS services for Caddy's sites.
- [DNS Spy](https://dnsspy.io) keeps an eye on Caddy's DNS properties.
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://github.com/sponsors/mholt)!**
## About the Project
Caddy was born out of the need for a "batteries-included" web server that runs anywhere and doesn't have to take its configuration with it. Caddy took inspiration from [spark](https://github.com/rif/spark), [nginx](https://github.com/nginx/nginx), lighttpd,
[Websocketd](https://github.com/joewalnes/websocketd) and [Vagrant](https://www.vagrantup.com/), which provides a pleasant mixture of features from each of them.
**The name "Caddy" is trademarked:** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". See [brand guidelines](https://caddyserver.com/brand). Caddy is a registered trademark of Light Code Labs, LLC.
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
-48
View File
@@ -1,48 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"os"
"path/filepath"
"runtime"
)
// AssetsPath returns the path to the folder
// where the application may store data. If
// CADDYPATH env variable is set, that value
// is used. Otherwise, the path is the result
// of evaluating "$HOME/.caddy".
func AssetsPath() string {
if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" {
return caddyPath
}
return filepath.Join(userHomeDir(), ".caddy")
}
// userHomeDir returns the user's home directory according to
// environment variables.
//
// Credit: http://stackoverflow.com/a/7922977/1048862
func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}
-1043
View File
File diff suppressed because it is too large Load Diff
-260
View File
@@ -1,260 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"errors"
"fmt"
"io"
"strings"
)
// Dispenser is a type that dispenses tokens, similarly to a lexer,
// except that it can do so with some notion of structure and has
// some really convenient methods.
type Dispenser struct {
filename string
tokens []Token
cursor int
nesting int
}
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
func NewDispenser(filename string, input io.Reader) Dispenser {
tokens, _ := allTokens(input) // ignoring error because nothing to do with it
return Dispenser{
filename: filename,
tokens: tokens,
cursor: -1,
}
}
// NewDispenserTokens returns a Dispenser filled with the given tokens.
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
return Dispenser{
filename: filename,
tokens: tokens,
cursor: -1,
}
}
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have been consumed.
func (d *Dispenser) Next() bool {
if d.cursor < len(d.tokens)-1 {
d.cursor++
return true
}
return false
}
// NextArg loads the next token if it is on the same
// line. Returns true if a token was loaded; false
// otherwise. If false, all tokens on the line have
// been consumed. It handles imported tokens correctly.
func (d *Dispenser) NextArg() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
d.cursor++
return true
}
return false
}
// NextLine loads the next token only if it is not on the same
// line as the current token, and returns true if a token was
// loaded; false otherwise. If false, there is not another token
// or it is on the same line. It handles imported tokens correctly.
func (d *Dispenser) NextLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
d.cursor++
return true
}
return false
}
// NextBlock can be used as the condition of a for loop
// to load the next token as long as it opens a block or
// is already in a block. It returns true if a token was
// loaded, or false when the block's closing curly brace
// was loaded and thus the block ended. Nested blocks are
// not supported.
func (d *Dispenser) NextBlock() bool {
if d.nesting > 0 {
d.Next()
if d.Val() == "}" {
d.nesting--
return false
}
return true
}
if !d.NextArg() { // block must open on same line
return false
}
if d.Val() != "{" {
d.cursor-- // roll back if not opening brace
return false
}
d.Next()
if d.Val() == "}" {
// Open and then closed right away
return false
}
d.nesting++
return true
}
// Val gets the text of the current token. If there is no token
// loaded, it returns empty string.
func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].Text
}
// Line gets the line number of the current token. If there is no token
// loaded, it returns 0.
func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0
}
return d.tokens[d.cursor].Line
}
// File gets the filename of the current token. If there is no token loaded,
// it returns the filename originally given when parsing started.
func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return d.filename
}
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
return tokenFilename
}
return d.filename
}
// Args is a convenience function that loads the next arguments
// (tokens on the same line) into an arbitrary number of strings
// pointed to in targets. If there are fewer tokens available
// than string pointers, the remaining strings will not be changed
// and false will be returned. If there were enough tokens available
// to fill the arguments, then true will be returned.
func (d *Dispenser) Args(targets ...*string) bool {
enough := true
for i := 0; i < len(targets); i++ {
if !d.NextArg() {
enough = false
break
}
*targets[i] = d.Val()
}
return enough
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in
// the return value nor is it loaded.
func (d *Dispenser) RemainingArgs() []string {
var args []string
for d.NextArg() {
if d.Val() == "{" {
d.cursor--
break
}
args = append(args, d.Val())
}
return args
}
// ArgErr returns an argument error, meaning that another
// argument was expected but not found. In other words,
// a line break or open curly brace was encountered instead of
// an argument.
func (d *Dispenser) ArgErr() error {
if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument")
}
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
}
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
return errors.New(msg)
}
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF")
}
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.Err(fmt.Sprintf(format, args...))
}
// numLineBreaks counts how many line breaks are in the token
// value given by the token index tknIdx. It returns 0 if the
// token does not exist or there are no line breaks.
func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0
}
return strings.Count(d.tokens[tknIdx].Text, "\n")
}
// isNewLine determines whether the current token is on a different
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
func (d *Dispenser) isNewLine() bool {
if d.cursor < 1 {
return true
}
if d.cursor > len(d.tokens)-1 {
return false
}
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
}
-198
View File
@@ -1,198 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
)
const filename = "Caddyfile"
// ToJSON converts caddyfile to its JSON representation.
func ToJSON(caddyfile []byte) ([]byte, error) {
var j EncodedCaddyfile
serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil)
if err != nil {
return nil, err
}
for _, sb := range serverBlocks {
block := EncodedServerBlock{
Keys: sb.Keys,
Body: [][]interface{}{},
}
// Extract directives deterministically by sorting them
var directives = make([]string, len(sb.Tokens))
for dir := range sb.Tokens {
directives = append(directives, dir)
}
sort.Strings(directives)
// Convert each directive's tokens into our JSON structure
for _, dir := range directives {
disp := NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() {
block.Body = append(block.Body, constructLine(&disp))
}
}
// tack this block onto the end of the list
j = append(j, block)
}
result, err := json.Marshal(j)
if err != nil {
return nil, err
}
return result, nil
}
// constructLine transforms tokens into a JSON-encodable structure;
// but only one line at a time, to be used at the top-level of
// a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level.
func constructLine(d *Dispenser) []interface{} {
var args []interface{}
args = append(args, d.Val())
for d.NextArg() {
if d.Val() == "{" {
args = append(args, constructBlock(d))
continue
}
args = append(args, d.Val())
}
return args
}
// constructBlock recursively processes tokens into a
// JSON-encodable structure. To be used in a directive's
// block. Goes to end of block.
func constructBlock(d *Dispenser) [][]interface{} {
block := [][]interface{}{}
for d.Next() {
if d.Val() == "}" {
break
}
block = append(block, constructLine(d))
}
return block
}
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
func FromJSON(jsonBytes []byte) ([]byte, error) {
var j EncodedCaddyfile
var result string
err := json.Unmarshal(jsonBytes, &j)
if err != nil {
return nil, err
}
for sbPos, sb := range j {
if sbPos > 0 {
result += "\n\n"
}
for i, key := range sb.Keys {
if i > 0 {
result += ", "
}
//result += standardizeScheme(key)
result += key
}
result += jsonToText(sb.Body, 1)
}
return []byte(result), nil
}
// jsonToText recursively transforms a scope of JSON into plain
// Caddyfile text.
func jsonToText(scope interface{}, depth int) string {
var result string
switch val := scope.(type) {
case string:
if strings.ContainsAny(val, "\" \n\t\r") {
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
} else {
result += val
}
case int:
result += strconv.Itoa(val)
case float64:
result += fmt.Sprintf("%v", val)
case bool:
result += fmt.Sprintf("%t", val)
case [][]interface{}:
result += " {\n"
for _, arg := range val {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
case []interface{}:
for i, v := range val {
if block, ok := v.([]interface{}); ok {
result += "{\n"
for _, arg := range block {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
continue
}
result += jsonToText(v, depth)
if i < len(val)-1 {
result += " "
}
}
}
return result
}
// TODO: Will this function come in handy somewhere else?
/*
// standardizeScheme turns an address like host:https into https://host,
// or "host:" into "host".
func standardizeScheme(addr string) string {
if hostname, port, err := net.SplitHostPort(addr); err == nil {
if port == "http" || port == "https" {
addr = port + "://" + hostname
}
}
return strings.TrimSuffix(addr, ":")
}
*/
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
type EncodedCaddyfile []EncodedServerBlock
// EncodedServerBlock represents a server block ripe for encoding.
type EncodedServerBlock struct {
Keys []string `json:"keys"`
Body [][]interface{} `json:"body"`
}
-153
View File
@@ -1,153 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"bufio"
"io"
"unicode"
)
type (
// lexer is a utility which can get values, token by
// token, from a Reader. A token is a word, and tokens
// are separated by whitespace. A word can be enclosed
// in quotes if it contains whitespace.
lexer struct {
reader *bufio.Reader
token Token
line int
}
// Token represents a single parsable unit.
Token struct {
File string
Line int
Text string
}
)
// load prepares the lexer to scan an input for tokens.
// It discards any leading byte order mark.
func (l *lexer) load(input io.Reader) error {
l.reader = bufio.NewReader(input)
l.line = 1
// discard byte order mark, if present
firstCh, _, err := l.reader.ReadRune()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if firstCh != 0xFEFF {
err := l.reader.UnreadRune()
if err != nil {
return err
}
}
return nil
}
// next loads the next token into the lexer.
// A token is delimited by whitespace, unless
// the token starts with a quotes character (")
// in which case the token goes until the closing
// quotes (the enclosing quotes are not included).
// Inside quoted strings, quotes may be escaped
// with a preceding \ character. No other chars
// may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
var val []rune
var comment, quoted, escaped bool
makeToken := func() bool {
l.token.Text = string(val)
return true
}
for {
ch, _, err := l.reader.ReadRune()
if err != nil {
if len(val) > 0 {
return makeToken()
}
if err == io.EOF {
return false
}
panic(err)
}
if quoted {
if !escaped {
if ch == '\\' {
escaped = true
continue
} else if ch == '"' {
quoted = false
return makeToken()
}
}
if ch == '\n' {
l.line++
}
if escaped {
// only escape quotes
if ch != '"' {
val = append(val, '\\')
}
}
val = append(val, ch)
escaped = false
continue
}
if unicode.IsSpace(ch) {
if ch == '\r' {
continue
}
if ch == '\n' {
l.line++
comment = false
}
if len(val) > 0 {
return makeToken()
}
continue
}
if ch == '#' {
comment = true
}
if comment {
continue
}
if len(val) == 0 {
l.token = Token{Line: l.line}
if ch == '"' {
quoted = true
continue
}
}
val = append(val, ch)
}
}
-490
View File
@@ -1,490 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"io"
"log"
"os"
"path/filepath"
"strings"
)
// Parse parses the input just enough to group tokens, in
// order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear.
// Directives that do not appear in validDirectives will cause
// an error. If you do not want to check for valid directives,
// pass in nil instead.
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
return p.parseAll()
}
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) ([]Token, error) {
l := new(lexer)
err := l.load(input)
if err != nil {
return nil, err
}
var tokens []Token
for l.next() {
tokens = append(tokens, l.token)
}
return tokens, nil
}
type parser struct {
Dispenser
block ServerBlock // current server block being parsed
validDirectives []string // a directive must be valid or it's an error
eof bool // if we encounter a valid EOF in a hard place
definedSnippets map[string][]Token
}
func (p *parser) parseAll() ([]ServerBlock, error) {
var blocks []ServerBlock
for p.Next() {
err := p.parseOne()
if err != nil {
return blocks, err
}
if len(p.block.Keys) > 0 {
blocks = append(blocks, p.block)
}
}
return blocks, nil
}
func (p *parser) parseOne() error {
p.block = ServerBlock{Tokens: make(map[string][]Token)}
return p.begin()
}
func (p *parser) begin() error {
if len(p.tokens) == 0 {
return nil
}
err := p.addresses()
if err != nil {
return err
}
if p.eof {
// this happens if the Caddyfile consists of only
// a line of addresses and nothing else
return nil
}
if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{}
}
if _, found := p.definedSnippets[name]; found {
return p.Errf("redeclaration of previously declared snippet %s", name)
}
// consume all tokens til matched close brace
tokens, err := p.snippetTokens()
if err != nil {
return err
}
p.definedSnippets[name] = tokens
// empty block keys so we don't save this block as a real server.
p.block.Keys = nil
return nil
}
return p.blockContents()
}
func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := replaceEnvVars(p.Val())
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
err := p.doImport()
if err != nil {
return err
}
continue
}
// Open brace definitely indicates end of addresses
if tkn == "{" {
if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
}
break
}
if tkn != "" { // empty token possible if user typed ""
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
if tkn[len(tkn)-1] == ',' {
tkn = tkn[:len(tkn)-1]
expectingAnother = true
} else {
expectingAnother = false // but we may still see another one on this line
}
p.block.Keys = append(p.block.Keys, tkn)
}
// Advance token and possibly break out of loop or return error
hasNext := p.Next()
if expectingAnother && !hasNext {
return p.EOFErr()
}
if !hasNext {
p.eof = true
break // EOF
}
if !expectingAnother && p.isNewLine() {
break
}
}
return nil
}
func (p *parser) blockContents() error {
errOpenCurlyBrace := p.openCurlyBrace()
if errOpenCurlyBrace != nil {
// single-server configs don't need curly braces
p.cursor--
}
err := p.directives()
if err != nil {
return err
}
// Only look for close curly brace if there was an opening
if errOpenCurlyBrace == nil {
err = p.closeCurlyBrace()
if err != nil {
return err
}
}
return nil
}
// directives parses through all the lines for directives
// and it expects the next token to be the first
// directive. It goes until EOF or closing curly brace
// which ends the server block.
func (p *parser) directives() error {
for p.Next() {
// end of server block
if p.Val() == "}" {
break
}
// special case: import directive replaces tokens during parse-time
if p.Val() == "import" {
err := p.doImport()
if err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
// normal case: parse a directive on this line
if err := p.directive(); err != nil {
return err
}
}
return nil
}
// doImport swaps out the import directive and its argument
// (a total of 2 tokens) with the tokens in the specified file
// or globbing pattern. When the function returns, the cursor
// is on the token before where the import directive was. In
// other words, call Next() to access the first token that was
// imported.
func (p *parser) doImport() error {
// syntax checks
if !p.NextArg() {
return p.ArgErr()
}
importPattern := replaceEnvVars(p.Val())
if importPattern == "" {
return p.Err("Import requires a non-empty filepath")
}
if p.NextArg() {
return p.Err("Import takes only one argument (glob pattern or file)")
}
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
// first check snippets. That is a simple, non-recursive replacement
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
importedTokens = p.definedSnippets[importPattern]
} else {
// make path relative to the file of the _token_ being processed rather
// than current working directory (issue #867) and then use glob to get
// list of matching filenames
absFile, err := filepath.Abs(p.Dispenser.File())
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}
var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
} else {
globPattern = importPattern
}
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
// See issue #2096 - a pattern with many glob expansions can hang for too long
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
}
matches, err = filepath.Glob(globPattern)
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
if strings.ContainsAny(globPattern, "*?[]") {
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
}
}
// collect all the imported tokens
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
return err
}
importedTokens = append(importedTokens, newTokens...)
}
}
// splice the imported tokens in the place of the import statement
// and rewind cursor so Next() will land on first imported token
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
p.cursor--
return nil
}
// doSingleImport lexes the individual file at importFile and returns
// its tokens or an error, if any.
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
file, err := os.Open(importFile)
if err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
}
defer file.Close()
if info, err := file.Stat(); err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
} else if info.IsDir() {
return nil, p.Errf("Could not import %s: is a directory", importFile)
}
importedTokens, err := allTokens(file)
if err != nil {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
}
// Tack the file path onto these tokens so errors show the imported file's name
// (we use full, absolute path to avoid bugs: issue #1892)
filename, err := filepath.Abs(importFile)
if err != nil {
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}
for i := 0; i < len(importedTokens); i++ {
importedTokens[i].File = filename
}
return importedTokens, nil
}
// directive collects tokens until the directive's scope
// closes (either end of line or end of curly brace block).
// It expects the currently-loaded token to be a directive
// (or } that ends a server block). The collected tokens
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
dir := replaceEnvVars(p.Val())
nesting := 0
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
if !p.validDirective(dir) {
return p.Errf("Unknown directive '%s'", dir)
}
// The directive itself is appended as a relevant token
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
for p.Next() {
if p.Val() == "{" {
nesting++
} else if p.isNewLine() && nesting == 0 {
p.cursor-- // read too far
break
} else if p.Val() == "}" && nesting > 0 {
nesting--
} else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
} else if p.Val() == "import" && p.isNewLine() {
if err := p.doImport(); err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
}
if nesting > 0 {
return p.EOFErr()
}
return nil
}
// openCurlyBrace expects the current token to be an
// opening curly brace. This acts like an assertion
// because it returns an error if the token is not
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
return p.SyntaxErr("{")
}
return nil
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
// a closing curly brace. It does NOT advance the token.
func (p *parser) closeCurlyBrace() error {
if p.Val() != "}" {
return p.SyntaxErr("}")
}
return nil
}
// validDirective returns true if dir is in p.validDirectives.
func (p *parser) validDirective(dir string) bool {
if p.validDirectives == nil {
return true
}
for _, d := range p.validDirectives {
if d == dir {
return true
}
}
return false
}
// replaceEnvVars replaces environment variables that appear in the token
// and understands both the $UNIX and %WINDOWS% syntaxes.
func replaceEnvVars(s string) string {
s = replaceEnvReferences(s, "{%", "%}")
s = replaceEnvReferences(s, "{$", "}")
return s
}
// replaceEnvReferences performs the actual replacement of env variables
// in s, given the placeholder start and placeholder end strings.
func replaceEnvReferences(s, refStart, refEnd string) string {
index := strings.Index(s, refStart)
for index != -1 {
endIndex := strings.Index(s[index:], refEnd)
if endIndex == -1 {
break
}
endIndex += index
if endIndex > index+len(refStart) {
ref := s[index : endIndex+len(refEnd)]
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
} else {
return s
}
index = strings.Index(s, refStart)
}
return s
}
// ServerBlock associates any number of keys (usually addresses
// of some sort) with tokens (grouped by directive name).
type ServerBlock struct {
Keys []string
Tokens map[string][]Token
}
func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][1:], ")")
}
return false, ""
}
// read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) {
// TODO: disallow imports in snippets for simplicity at import time
// snippet must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
count := 1
tokens := []Token{}
for p.Next() {
if p.Val() == "}" {
count--
if count == 0 {
break
}
}
if p.Val() == "{" {
count++
}
tokens = append(tokens, p.tokens[p.cursor])
}
// make sure we're matched up
if count != 0 {
return nil, p.SyntaxErr("}")
}
return tokens, nil
}
-133
View File
@@ -1,133 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"errors"
"runtime"
"unicode"
"github.com/flynn/go-shlex"
)
var runtimeGoos = runtime.GOOS
// SplitCommandAndArgs takes a command string and parses it shell-style into the
// command and its separate arguments.
func SplitCommandAndArgs(command string) (cmd string, args []string, err error) {
var parts []string
if runtimeGoos == "windows" {
parts = parseWindowsCommand(command) // parse it Windows-style
} else {
parts, err = parseUnixCommand(command) // parse it Unix-style
if err != nil {
err = errors.New("error parsing command: " + err.Error())
return
}
}
if len(parts) == 0 {
err = errors.New("no command contained in '" + command + "'")
return
}
cmd = parts[0]
if len(parts) > 1 {
args = parts[1:]
}
return
}
// parseUnixCommand parses a unix style command line and returns the
// command and its arguments or an error
func parseUnixCommand(cmd string) ([]string, error) {
return shlex.Split(cmd)
}
// parseWindowsCommand parses windows command lines and
// returns the command and the arguments as an array. It
// should be able to parse commonly used command lines.
// Only basic syntax is supported:
// - spaces in double quotes are not token delimiters
// - double quotes are escaped by either backspace or another double quote
// - except for the above case backspaces are path separators (not special)
//
// Many sources point out that escaping quotes using backslash can be unsafe.
// Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 )
//
// This function has to be used on Windows instead
// of the shlex package because this function treats backslash
// characters properly.
func parseWindowsCommand(cmd string) []string {
const backslash = '\\'
const quote = '"'
var parts []string
var part string
var inQuotes bool
var lastRune rune
for i, ch := range cmd {
if i != 0 {
lastRune = rune(cmd[i-1])
}
if ch == backslash {
// put it in the part - for now we don't know if it's an
// escaping char or path separator
part += string(ch)
continue
}
if ch == quote {
if lastRune == backslash {
// remove the backslash from the part and add the escaped quote instead
part = part[:len(part)-1]
part += string(ch)
continue
}
if lastRune == quote {
// revert the last change of the inQuotes state
// it was an escaping quote
inQuotes = !inQuotes
part += string(ch)
continue
}
// normal escaping quotes
inQuotes = !inQuotes
continue
}
if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 {
parts = append(parts, part)
part = ""
continue
}
part += string(ch)
}
if len(part) > 0 {
parts = append(parts, part)
}
return parts
}
-145
View File
@@ -1,145 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"strings"
"github.com/coredns/caddy/caddyfile"
)
// Controller is given to the setup function of directives which
// gives them access to be able to read tokens with which to
// configure themselves. It also stores state for the setup
// functions, can get the current context, and can be used to
// identify a particular server block using the Key field.
type Controller struct {
caddyfile.Dispenser
// The instance in which the setup is occurring
instance *Instance
// Key is the key from the top of the server block, usually
// an address, hostname, or identifier of some sort.
Key string
// OncePerServerBlock is a function that executes f
// exactly once per server block, no matter how many
// hosts are associated with it. If it is the first
// time, the function f is executed immediately
// (not deferred) and may return an error which is
// returned by OncePerServerBlock.
OncePerServerBlock func(f func() error) error
// ServerBlockIndex is the 0-based index of the
// server block as it appeared in the input.
ServerBlockIndex int
// ServerBlockKeyIndex is the 0-based index of this
// key as it appeared in the input at the head of the
// server block.
ServerBlockKeyIndex int
// ServerBlockKeys is a list of keys that are
// associated with this server block. All these
// keys, consequently, share the same tokens.
ServerBlockKeys []string
// ServerBlockStorage is used by a directive's
// setup function to persist state between all
// the keys on a server block.
ServerBlockStorage interface{}
}
// ServerType gets the name of the server type that is being set up.
func (c *Controller) ServerType() string {
return c.instance.serverType
}
// OnFirstStartup adds fn to the list of callback functions to execute
// when the server is about to be started NOT as part of a restart.
func (c *Controller) OnFirstStartup(fn func() error) {
c.instance.OnFirstStartup = append(c.instance.OnFirstStartup, fn)
}
// OnStartup adds fn to the list of callback functions to execute
// when the server is about to be started (including restarts).
func (c *Controller) OnStartup(fn func() error) {
c.instance.OnStartup = append(c.instance.OnStartup, fn)
}
// OnRestart adds fn to the list of callback functions to execute
// when the server is about to be restarted.
func (c *Controller) OnRestart(fn func() error) {
c.instance.OnRestart = append(c.instance.OnRestart, fn)
}
// OnRestartFailed adds fn to the list of callback functions to execute
// if the server failed to restart.
func (c *Controller) OnRestartFailed(fn func() error) {
c.instance.OnRestartFailed = append(c.instance.OnRestartFailed, fn)
}
// OnShutdown adds fn to the list of callback functions to execute
// when the server is about to be shut down (including restarts).
func (c *Controller) OnShutdown(fn func() error) {
c.instance.OnShutdown = append(c.instance.OnShutdown, fn)
}
// OnFinalShutdown adds fn to the list of callback functions to execute
// when the server is about to be shut down NOT as part of a restart.
func (c *Controller) OnFinalShutdown(fn func() error) {
c.instance.OnFinalShutdown = append(c.instance.OnFinalShutdown, fn)
}
// Context gets the context associated with the instance associated with c.
func (c *Controller) Context() Context {
return c.instance.context
}
// Get safely gets a value from the Instance's storage.
func (c *Controller) Get(key interface{}) interface{} {
c.instance.StorageMu.RLock()
defer c.instance.StorageMu.RUnlock()
return c.instance.Storage[key]
}
// Set safely sets a value on the Instance's storage.
func (c *Controller) Set(key, val interface{}) {
c.instance.StorageMu.Lock()
c.instance.Storage[key] = val
c.instance.StorageMu.Unlock()
}
// NewTestController creates a new Controller for
// the server type and input specified. The filename
// is "Testfile". If the server type is not empty and
// is plugged in, a context will be created so that
// the results of setup functions can be checked for
// correctness.
//
// Used only for testing, but exported so plugins can
// use this for convenience.
func NewTestController(serverType, input string) *Controller {
testInst := &Instance{serverType: serverType, Storage: make(map[interface{}]interface{})}
if stype, err := getServerType(serverType); err == nil {
testInst.context = stype.NewContext(testInst)
}
return &Controller{
instance: testInst,
Dispenser: caddyfile.NewDispenser("Testfile", strings.NewReader(input)),
OncePerServerBlock: func(f func() error) error { return f() },
}
}
-450
View File
@@ -1,450 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"fmt"
"log"
"net"
"sort"
"sync"
"github.com/coredns/caddy/caddyfile"
)
// These are all the registered plugins.
var (
// serverTypes is a map of registered server types.
serverTypes = make(map[string]ServerType)
// plugins is a map of server type to map of plugin name to
// Plugin. These are the "general" plugins that may or may
// not be associated with a specific server type. If it's
// applicable to multiple server types or the server type is
// irrelevant, the key is empty string (""). But all plugins
// must have a name.
plugins = make(map[string]map[string]Plugin)
// eventHooks is a map of hook name to Hook. All hooks plugins
// must have a name.
eventHooks = &sync.Map{}
// parsingCallbacks maps server type to map of directive
// to list of callback functions. These aren't really
// plugins on their own, but are often registered from
// plugins.
parsingCallbacks = make(map[string]map[string][]ParsingCallback)
// caddyfileLoaders is the list of all Caddyfile loaders
// in registration order.
caddyfileLoaders []caddyfileLoader
)
// DescribePlugins returns a string describing the registered plugins.
func DescribePlugins() string {
pl := ListPlugins()
str := ""
for _, name := range pl["others"] {
if len(name) > 3 {
str += name[4:] + "\n" // drop dns. prefix caddy adds
} else {
str += name + "\n"
}
}
return str
}
// ListPlugins makes a list of the registered plugins,
// keyed by plugin type.
func ListPlugins() map[string][]string {
p := make(map[string][]string)
// server type plugins
for name := range serverTypes {
p["server_types"] = append(p["server_types"], name)
}
// caddyfile loaders in registration order
for _, loader := range caddyfileLoaders {
p["caddyfile_loaders"] = append(p["caddyfile_loaders"], loader.name)
}
if defaultCaddyfileLoader.name != "" {
p["caddyfile_loaders"] = append(p["caddyfile_loaders"], defaultCaddyfileLoader.name)
}
// List the event hook plugins
eventHooks.Range(func(k, _ interface{}) bool {
p["event_hooks"] = append(p["event_hooks"], k.(string))
return true
})
// alphabetize the rest of the plugins
var others []string
for stype, stypePlugins := range plugins {
for name := range stypePlugins {
var s string
if stype != "" {
s = stype + "."
}
s += name
others = append(others, s)
}
}
sort.Strings(others)
for _, name := range others {
p["others"] = append(p["others"], name)
}
return p
}
// ValidDirectives returns the list of all directives that are
// recognized for the server type serverType. However, not all
// directives may be installed. This makes it possible to give
// more helpful error messages, like "did you mean ..." or
// "maybe you need to plug in ...".
func ValidDirectives(serverType string) []string {
stype, err := getServerType(serverType)
if err != nil {
return nil
}
return stype.Directives()
}
// ServerListener pairs a server to its listener and/or packetconn.
type ServerListener struct {
server Server
listener net.Listener
packet net.PacketConn
}
// LocalAddr returns the local network address of the packetconn. It returns
// nil when it is not set.
func (s ServerListener) LocalAddr() net.Addr {
if s.packet == nil {
return nil
}
return s.packet.LocalAddr()
}
// Addr returns the listener's network address. It returns nil when it is
// not set.
func (s ServerListener) Addr() net.Addr {
if s.listener == nil {
return nil
}
return s.listener.Addr()
}
// Context is a type which carries a server type through
// the load and setup phase; it maintains the state
// between loading the Caddyfile, then executing its
// directives, then making the servers for Caddy to
// manage. Typically, such state involves configuration
// structs, etc.
type Context interface {
// Called after the Caddyfile is parsed into server
// blocks but before the directives are executed,
// this method gives you an opportunity to inspect
// the server blocks and prepare for the execution
// of directives. Return the server blocks (which
// you may modify, if desired) and an error, if any.
// The first argument is the name or path to the
// configuration file (Caddyfile).
//
// This function can be a no-op and simply return its
// input if there is nothing to do here.
InspectServerBlocks(string, []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error)
// This is what Caddy calls to make server instances.
// By this time, all directives have been executed and,
// presumably, the context has enough state to produce
// server instances for Caddy to start.
MakeServers() ([]Server, error)
}
// RegisterServerType registers a server type srv by its
// name, typeName.
func RegisterServerType(typeName string, srv ServerType) {
if _, ok := serverTypes[typeName]; ok {
panic("server type already registered")
}
serverTypes[typeName] = srv
}
// ServerType contains information about a server type.
type ServerType struct {
// Function that returns the list of directives, in
// execution order, that are valid for this server
// type. Directives should be one word if possible
// and lower-cased.
Directives func() []string
// DefaultInput returns a default config input if none
// is otherwise loaded. This is optional, but highly
// recommended, otherwise a blank Caddyfile will be
// used.
DefaultInput func() Input
// The function that produces a new server type context.
// This will be called when a new Caddyfile is being
// loaded, parsed, and executed independently of any
// startup phases before this one. It's a way to keep
// each set of server instances separate and to reduce
// the amount of global state you need.
NewContext func(inst *Instance) Context
}
// Plugin is a type which holds information about a plugin.
type Plugin struct {
// ServerType is the type of server this plugin is for.
// Can be empty if not applicable, or if the plugin
// can associate with any server type.
ServerType string
// Action is the plugin's setup function, if associated
// with a directive in the Caddyfile.
Action SetupFunc
}
// RegisterPlugin plugs in plugin. All plugins should register
// themselves, even if they do not perform an action associated
// with a directive. It is important for the process to know
// which plugins are available.
//
// The plugin MUST have a name: lower case and one word.
// If this plugin has an action, it must be the name of
// the directive that invokes it. A name is always required
// and must be unique for the server type.
func RegisterPlugin(name string, plugin Plugin) {
if name == "" {
panic("plugin must have a name")
}
if _, ok := plugins[plugin.ServerType]; !ok {
plugins[plugin.ServerType] = make(map[string]Plugin)
}
if _, dup := plugins[plugin.ServerType][name]; dup {
panic("plugin named " + name + " already registered for server type " + plugin.ServerType)
}
plugins[plugin.ServerType][name] = plugin
}
// EventName represents the name of an event used with event hooks.
type EventName string
// Define names for the various events
const (
StartupEvent EventName = "startup"
ShutdownEvent = "shutdown"
CertRenewEvent = "certrenew"
InstanceStartupEvent = "instancestartup"
InstanceRestartEvent = "instancerestart"
)
// EventHook is a type which holds information about a startup hook plugin.
type EventHook func(eventType EventName, eventInfo interface{}) error
// RegisterEventHook plugs in hook. All the hooks should register themselves
// and they must have a name.
func RegisterEventHook(name string, hook EventHook) {
if name == "" {
panic("event hook must have a name")
}
_, dup := eventHooks.LoadOrStore(name, hook)
if dup {
panic("hook named " + name + " already registered")
}
}
// EmitEvent executes the different hooks passing the EventType as an
// argument. This is a blocking function. Hook developers should
// use 'go' keyword if they don't want to block Caddy.
func EmitEvent(event EventName, info interface{}) {
eventHooks.Range(func(k, v interface{}) bool {
err := v.(EventHook)(event, info)
if err != nil {
log.Printf("error on '%s' hook: %v", k.(string), err)
}
return true
})
}
// cloneEventHooks return a clone of the event hooks *sync.Map
func cloneEventHooks() *sync.Map {
c := &sync.Map{}
eventHooks.Range(func(k, v interface{}) bool {
c.Store(k, v)
return true
})
return c
}
// purgeEventHooks purges all event hooks from the map
func purgeEventHooks() {
eventHooks.Range(func(k, _ interface{}) bool {
eventHooks.Delete(k)
return true
})
}
// restoreEventHooks restores eventHooks with a provided *sync.Map
func restoreEventHooks(m *sync.Map) {
// Purge old event hooks
purgeEventHooks()
// Restore event hooks
m.Range(func(k, v interface{}) bool {
eventHooks.Store(k, v)
return true
})
}
// ParsingCallback is a function that is called after
// a directive's setup functions have been executed
// for all the server blocks.
type ParsingCallback func(Context) error
// RegisterParsingCallback registers callback to be called after
// executing the directive afterDir for server type serverType.
func RegisterParsingCallback(serverType, afterDir string, callback ParsingCallback) {
if _, ok := parsingCallbacks[serverType]; !ok {
parsingCallbacks[serverType] = make(map[string][]ParsingCallback)
}
parsingCallbacks[serverType][afterDir] = append(parsingCallbacks[serverType][afterDir], callback)
}
// SetupFunc is used to set up a plugin, or in other words,
// execute a directive. It will be called once per key for
// each server block it appears in.
type SetupFunc func(c *Controller) error
// DirectiveAction gets the action for directive dir of
// server type serverType.
func DirectiveAction(serverType, dir string) (SetupFunc, error) {
if stypePlugins, ok := plugins[serverType]; ok {
if plugin, ok := stypePlugins[dir]; ok {
return plugin.Action, nil
}
}
if genericPlugins, ok := plugins[""]; ok {
if plugin, ok := genericPlugins[dir]; ok {
return plugin.Action, nil
}
}
return nil, fmt.Errorf("no action found for directive '%s' with server type '%s' (missing a plugin?)",
dir, serverType)
}
// Loader is a type that can load a Caddyfile.
// It is passed the name of the server type.
// It returns an error only if something went
// wrong, not simply if there is no Caddyfile
// for this loader to load.
//
// A Loader should only load the Caddyfile if
// a certain condition or requirement is met,
// as returning a non-nil Input value along with
// another Loader will result in an error.
// In other words, loading the Caddyfile must
// be deliberate & deterministic, not haphazard.
//
// The exception is the default Caddyfile loader,
// which will be called only if no other Caddyfile
// loaders return a non-nil Input. The default
// loader may always return an Input value.
type Loader interface {
Load(serverType string) (Input, error)
}
// LoaderFunc is a convenience type similar to http.HandlerFunc
// that allows you to use a plain function as a Load() method.
type LoaderFunc func(serverType string) (Input, error)
// Load loads a Caddyfile.
func (lf LoaderFunc) Load(serverType string) (Input, error) {
return lf(serverType)
}
// RegisterCaddyfileLoader registers loader named name.
func RegisterCaddyfileLoader(name string, loader Loader) {
caddyfileLoaders = append(caddyfileLoaders, caddyfileLoader{name: name, loader: loader})
}
// SetDefaultCaddyfileLoader registers loader by name
// as the default Caddyfile loader if no others produce
// a Caddyfile. If another Caddyfile loader has already
// been set as the default, this replaces it.
//
// Do not call RegisterCaddyfileLoader on the same
// loader; that would be redundant.
func SetDefaultCaddyfileLoader(name string, loader Loader) {
defaultCaddyfileLoader = caddyfileLoader{name: name, loader: loader}
}
// loadCaddyfileInput iterates the registered Caddyfile loaders
// and, if needed, calls the default loader, to load a Caddyfile.
// It is an error if any of the loaders return an error or if
// more than one loader returns a Caddyfile.
func loadCaddyfileInput(serverType string) (Input, error) {
var loadedBy string
var caddyfileToUse Input
for _, l := range caddyfileLoaders {
cdyfile, err := l.loader.Load(serverType)
if err != nil {
return nil, fmt.Errorf("loading Caddyfile via %s: %v", l.name, err)
}
if cdyfile != nil {
if caddyfileToUse != nil {
return nil, fmt.Errorf("Caddyfile loaded multiple times; first by %s, then by %s", loadedBy, l.name)
}
loaderUsed = l
caddyfileToUse = cdyfile
loadedBy = l.name
}
}
if caddyfileToUse == nil && defaultCaddyfileLoader.loader != nil {
cdyfile, err := defaultCaddyfileLoader.loader.Load(serverType)
if err != nil {
return nil, err
}
if cdyfile != nil {
loaderUsed = defaultCaddyfileLoader
caddyfileToUse = cdyfile
}
}
return caddyfileToUse, nil
}
// OnProcessExit is a list of functions to run when the process
// exits -- they are ONLY for cleanup and should not block,
// return errors, or do anything fancy. They will be run with
// every signal, even if "shutdown callbacks" are not executed.
// This variable must only be modified in the main goroutine
// from init() functions.
var OnProcessExit []func()
// caddyfileLoader pairs the name of a loader to the loader.
type caddyfileLoader struct {
name string
loader Loader
}
var (
defaultCaddyfileLoader caddyfileLoader // the default loader if all else fail
loaderUsed caddyfileLoader // the loader that was used (relevant for reloads)
)
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build windows plan9 nacl js
package caddy
// checkFdlimit issues a warning if the OS limit for
// max file descriptors is below a recommended minimum.
func checkFdlimit() {
}
-37
View File
@@ -1,37 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows,!plan9,!nacl,!js
package caddy
import (
"fmt"
"syscall"
)
// checkFdlimit issues a warning if the OS limit for
// max file descriptors is below a recommended minimum.
func checkFdlimit() {
const min = 8192
// Warn if ulimit is too low for production sites
rlimit := &syscall.Rlimit{}
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rlimit)
if err == nil && rlimit.Cur < min {
fmt.Printf("WARNING: File descriptor limit %d is too low for production servers. "+
"At least %d is recommended. Fix with `ulimit -n %d`.\n", rlimit.Cur, min, min)
}
}
-103
View File
@@ -1,103 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"log"
"os"
"os/signal"
"sync"
)
// TrapSignals create signal handlers for all applicable signals for this
// system. If your Go program uses signals, this is a rather invasive
// function; best to implement them yourself in that case. Signals are not
// required for the caddy package to function properly, but this is a
// convenient way to allow the user to control this part of your program.
func TrapSignals() {
trapSignalsCrossPlatform()
trapSignalsPosix()
}
// trapSignalsCrossPlatform captures SIGINT, which triggers forceful
// shutdown that executes shutdown callbacks first. A second interrupt
// signal will exit the process immediately.
func trapSignalsCrossPlatform() {
go func() {
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt)
for i := 0; true; i++ {
<-shutdown
if i > 0 {
log.Println("[INFO] SIGINT: Force quit")
for _, f := range OnProcessExit {
f() // important cleanup actions only
}
os.Exit(2)
}
log.Println("[INFO] SIGINT: Shutting down")
// important cleanup actions before shutdown callbacks
for _, f := range OnProcessExit {
f()
}
go func() {
os.Exit(executeShutdownCallbacks("SIGINT"))
}()
}
}()
}
// executeShutdownCallbacks executes the shutdown callbacks as initiated
// by signame. It logs any errors and returns the recommended exit status.
// This function is idempotent; subsequent invocations always return 0.
func executeShutdownCallbacks(signame string) (exitCode int) {
shutdownCallbacksOnce.Do(func() {
// execute third-party shutdown hooks
EmitEvent(ShutdownEvent, signame)
errs := allShutdownCallbacks()
if len(errs) > 0 {
for _, err := range errs {
log.Printf("[ERROR] %s shutdown: %v", signame, err)
}
exitCode = 4
}
})
return
}
// allShutdownCallbacks executes all the shutdown callbacks
// for all the instances, and returns all the errors generated
// during their execution. An error executing one shutdown
// callback does not stop execution of others. Only one shutdown
// callback is executed at a time.
func allShutdownCallbacks() []error {
var errs []error
instancesMu.Lock()
for _, inst := range instances {
errs = append(errs, inst.ShutdownCallbacks()...)
}
instancesMu.Unlock()
return errs
}
// shutdownCallbacksOnce ensures that shutdown callbacks
// for all instances are only executed once.
var shutdownCallbacksOnce sync.Once
-19
View File
@@ -1,19 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build windows plan9 nacl js
package caddy
func trapSignalsPosix() {}
-106
View File
@@ -1,106 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows,!plan9,!nacl,!js
package caddy
import (
"log"
"os"
"os/signal"
"syscall"
)
// trapSignalsPosix captures POSIX-only signals.
func trapSignalsPosix() {
go func() {
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
for sig := range sigchan {
switch sig {
case syscall.SIGQUIT:
log.Println("[INFO] SIGQUIT: Quitting process immediately")
for _, f := range OnProcessExit {
f() // only perform important cleanup actions
}
os.Exit(0)
case syscall.SIGTERM:
log.Println("[INFO] SIGTERM: Shutting down servers then terminating")
exitCode := executeShutdownCallbacks("SIGTERM")
for _, f := range OnProcessExit {
f() // only perform important cleanup actions
}
err := Stop()
if err != nil {
log.Printf("[ERROR] SIGTERM stop: %v", err)
exitCode = 3
}
os.Exit(exitCode)
case syscall.SIGUSR1:
log.Println("[INFO] SIGUSR1: Reloading")
// Start with the existing Caddyfile
caddyfileToUse, inst, err := getCurrentCaddyfile()
if err != nil {
log.Printf("[ERROR] SIGUSR1: %v", err)
continue
}
if loaderUsed.loader == nil {
// This also should never happen
log.Println("[ERROR] SIGUSR1: no Caddyfile loader with which to reload Caddyfile")
continue
}
// Load the updated Caddyfile
newCaddyfile, err := loaderUsed.loader.Load(inst.serverType)
if err != nil {
log.Printf("[ERROR] SIGUSR1: loading updated Caddyfile: %v", err)
continue
}
if newCaddyfile != nil {
caddyfileToUse = newCaddyfile
}
// Backup old event hooks
oldEventHooks := cloneEventHooks()
// Purge the old event hooks
purgeEventHooks()
// Kick off the restart; our work is done
EmitEvent(InstanceRestartEvent, nil)
_, err = inst.Restart(caddyfileToUse)
if err != nil {
restoreEventHooks(oldEventHooks)
log.Printf("[ERROR] SIGUSR1: %v", err)
}
case syscall.SIGUSR2:
log.Println("[INFO] SIGUSR2: Upgrading")
if err := Upgrade(); err != nil {
log.Printf("[ERROR] SIGUSR2: upgrading: %v", err)
}
case syscall.SIGHUP:
// ignore; this signal is sometimes sent outside of the user's control
}
}
}()
}
-232
View File
@@ -1,232 +0,0 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"encoding/gob"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"sync"
)
func init() {
// register CaddyfileInput with gob so it knows into
// which concrete type to decode an Input interface
gob.Register(CaddyfileInput{})
}
// IsUpgrade returns true if this process is part of an upgrade
// where a parent caddy process spawned this one to upgrade
// the binary.
func IsUpgrade() bool {
mu.Lock()
defer mu.Unlock()
return isUpgrade
}
// Upgrade re-launches the process, preserving the listeners
// for a graceful upgrade. It does NOT load new configuration;
// it only starts the process anew with the current config.
// This makes it possible to perform zero-downtime binary upgrades.
//
// TODO: For more information when debugging, see:
// https://forum.golangbridge.org/t/bind-address-already-in-use-even-after-listener-closed/1510?u=matt
// https://github.com/mholt/shared-conn
func Upgrade() error {
log.Println("[INFO] Upgrading")
// use existing Caddyfile; do not change configuration during upgrade
currentCaddyfile, _, err := getCurrentCaddyfile()
if err != nil {
return err
}
if len(os.Args) == 0 { // this should never happen, but...
os.Args = []string{""}
}
// tell the child that it's a restart
env := os.Environ()
if !IsUpgrade() {
env = append(env, "CADDY__UPGRADE=1")
}
// prepare our payload to the child process
cdyfileGob := transferGob{
ListenerFds: make(map[string]uintptr),
Caddyfile: currentCaddyfile,
}
// prepare a pipe to the fork's stdin so it can get the Caddyfile
rpipe, wpipe, err := os.Pipe()
if err != nil {
return err
}
// prepare a pipe that the child process will use to communicate
// its success with us by sending > 0 bytes
sigrpipe, sigwpipe, err := os.Pipe()
if err != nil {
return err
}
// pass along relevant file descriptors to child process; ordering
// is very important since we rely on these being in certain positions.
extraFiles := []*os.File{sigwpipe} // fd 3
// add file descriptors of all the sockets
for i, j := 0, 0; ; i++ {
instancesMu.Lock()
if i >= len(instances) {
instancesMu.Unlock()
break
}
inst := instances[i]
instancesMu.Unlock()
for _, s := range inst.servers {
gs, gracefulOk := s.server.(GracefulServer)
ln, lnOk := s.listener.(Listener)
pc, pcOk := s.packet.(PacketConn)
if gracefulOk {
if lnOk {
lnFile, _ := ln.File()
extraFiles = append(extraFiles, lnFile)
cdyfileGob.ListenerFds["tcp"+gs.Address()] = uintptr(4 + j) // 4 fds come before any of the listeners
j++
}
if pcOk {
pcFile, _ := pc.File()
extraFiles = append(extraFiles, pcFile)
cdyfileGob.ListenerFds["udp"+gs.Address()] = uintptr(4 + j) // 4 fds come before any of the listeners
j++
}
}
}
}
// set up the command
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.Stdin = rpipe // fd 0
cmd.Stdout = os.Stdout // fd 1
cmd.Stderr = os.Stderr // fd 2
cmd.ExtraFiles = extraFiles
cmd.Env = env
// spawn the child process
err = cmd.Start()
if err != nil {
return err
}
// immediately close our dup'ed fds and the write end of our signal pipe
for _, f := range extraFiles {
err = f.Close()
if err != nil {
return err
}
}
// feed Caddyfile to the child
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
if err != nil {
return err
}
err = wpipe.Close()
if err != nil {
return err
}
// determine whether child startup succeeded
answer, readErr := ioutil.ReadAll(sigrpipe)
if len(answer) == 0 {
cmdErr := cmd.Wait() // get exit status
errStr := fmt.Sprintf("child failed to initialize: %v", cmdErr)
if readErr != nil {
errStr += fmt.Sprintf(" - additionally, error communicating with child process: %v", readErr)
}
return fmt.Errorf(errStr)
}
// looks like child is successful; we can exit gracefully.
log.Println("[INFO] Upgrade finished")
return Stop()
}
// getCurrentCaddyfile gets the Caddyfile used by the
// current (first) Instance and returns both of them.
func getCurrentCaddyfile() (Input, *Instance, error) {
instancesMu.Lock()
if len(instances) == 0 {
instancesMu.Unlock()
return nil, nil, fmt.Errorf("no server instances are fully running")
}
inst := instances[0]
instancesMu.Unlock()
currentCaddyfile := inst.caddyfileInput
if currentCaddyfile == nil {
// hmm, did spawning process forget to close stdin? Anyhow, this is unusual.
return nil, inst, fmt.Errorf("no Caddyfile to reload (was stdin left open?)")
}
return currentCaddyfile, inst, nil
}
// signalSuccessToParent tells the parent our status using pipe at index 3.
// If this process is not a restart, this function does nothing.
// Calling this function once this process has successfully initialized
// is vital so that the parent process can unblock and kill itself.
// This function is idempotent; it executes at most once per process.
func signalSuccessToParent() {
signalParentOnce.Do(func() {
if IsUpgrade() {
ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3
_, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent
if err != nil {
log.Printf("[ERROR] Communicating successful init to parent: %v", err)
}
ppipe.Close()
}
})
}
// signalParentOnce is used to make sure that the parent is only
// signaled once; doing so more than once breaks whatever socket is
// at fd 4 (TODO: the reason for this is still unclear - to reproduce,
// call Stop() and Start() in succession at least once after a
// restart, then try loading first host of Caddyfile in the browser
// - this was pre-v0.9; this code and godoc is borrowed from the
// implementation then, but I'm not sure if it's been fixed yet, as
// of v0.10.7). Do not use this directly; call signalSuccessToParent
// instead.
var signalParentOnce sync.Once
// transferGob is used if this is a child process as part of
// a graceful upgrade; it is used to map listeners to their
// index in the list of inherited file descriptors. This
// variable is not safe for concurrent access.
var loadedGob transferGob
// transferGob maps bind address to index of the file descriptor
// in the Files array passed to the child process. It also contains
// the Caddyfile contents and any other state needed by the new process.
// Used only during graceful upgrades.
type transferGob struct {
ListenerFds map[string]uintptr
Caddyfile Input
}
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016-2020 The CoreDNS authors and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Some files were not shown because too many files have changed in this diff Show More