Compare commits

...

35 Commits

Author SHA1 Message Date
João "Pisco" Fernandes ecb88678f1 Release 2026.7.1 2026-07-09 13:06:21 +01:00
Miguel da Costa Martins Marcelino 43bfec0bcd Revert "TUN-10621: Propagate max wait timeout"
This reverts merge request !1859
2026-07-09 13:00:02 +01:00
João "Pisco" Fernandes 86fccede6d Release 2026.7.0 2026-07-08 13:08:05 +01:00
João "Pisco" Fernandes a53d9e5d44 Revert "TUN-10557: Bump quic-go v0.59.1"
This reverts commit 02eb75b56d.
2026-07-08 11:48:16 +01:00
João "Pisco" Fernandes 9171757c66 chore: Bump go-chi to version 5.3.1 2026-07-08 11:36:00 +01:00
Miguel da Costa Martins Marcelino c06b2d43e9 TUN-10621: Test websocket path
Adding a test that tries to exercise the race condition for the websocket path. The sequence is as follows:

1. The test starts by writing the payload to the mock server
2. Then, it closes the write side of the connection.
3. It waits for 3 seconds to make the race window explicit: the bug in cloudflared would have closed the connection and we would not be able to read.
4. Read the message.

With the fix applied, we should be able to read the full message.
2026-07-01 18:27:12 +01:00
Renovate a9d023b7a2 Migrate config renovate.json 2026-06-30 17:19:15 +01:00
João "Pisco" Fernandes f76f75b449 ci: add Semgrep OSS scanning workflow
## Summary
Port from https://github.com/cloudflare/cloudflared/pull/1639 to GitLab
2026-06-30 09:36:55 +01:00
Miguel da Costa Martins Marcelino 5c66bd68ab TUN-10621: Propagate max wait timeout
This PR addresses an issue where cloudflared prematurely closes the origin connection before the upstream-to-downstream goroutine finishes reading, causing intermittent connection drops when a client immediately closes the write-side of a connection.

When a client finishes writing data, it immediately closes its side of the connection. Under the current implementation in cloudflared's downstream-to-upstream goroutine does not wait for the second stream to complete. It unblocks the moment the first stream writes to the channel. Once this happens, the pipe returns control to `proxyTCPStream`, which prematurely closes the origin connection. Consequently, when the upstream-to-downstream goroutine attempts to read the remaining data from the origin connection, the connection is already gone, leading to unexpected failures.

We started propagating the `TimeoutAfterFirstClose` configuration/parameter. This allows the proxy to wait for a designated period, giving the second stream sufficient time to finish processing and read all remaining data before `proxyTCPStream` tears down the origin connection.
2026-06-29 10:57:12 +00:00
Christopher Meng dba2d33a6b Consolidate init-system detection into shared inits package 2026-06-25 17:24:46 +00:00
Christopher Meng 6b0571b598 Fix lint issues 2026-06-25 17:24:46 +00:00
Christopher Meng d5e0c2bb89 Fix broken installed-service detection in systemd uninstall
systemctl list-units exits 0 whether or not any pattern matches, so the
filter was a no-op and every unit was always treated as installed.
Detect installed units by checking for the unit file on disk instead.
2026-06-25 17:24:46 +00:00
Christopher Meng 77e8965e28 Add OpenRC support
Generate a clean init script at /etc/init.d/cloudflared backed by
OpenRC's supervise-daemon supervisor, plus a companion
/etc/conf.d/cloudflared for operator overrides.
2026-06-25 17:24:46 +00:00
Renovate 8066821462 Pin dependencies
This MR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| gcr.io/distroless/base-debian13 | final | pinDigest |  → `ab7554b` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At 12:00 AM through 04:59 AM and 10:00 PM through 11:59 PM, Monday through Friday (`* 0-4,22-23 * * 1-5`)
  - Only on Sunday and Saturday (`* * * * 0,6`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yMjQuMCIsInVwZGF0ZWRJblZlciI6IjQzLjIyNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->
2026-06-25 17:12:51 +00:00
Miguel da Costa Martins Marcelino 1e4ebe5471 chore: Fix renovate
Fixing renovate in cloudflared.
2026-06-25 16:36:46 +00:00
Miguel da Costa Martins Marcelino 02eb75b56d TUN-10557: Bump quic-go v0.59.1
This adds back the quic-go bump.
2026-06-18 18:20:39 +00:00
MiguelMarcelino 81a53555aa Release 2026.6.1 2026-06-18 14:39:02 +01:00
Miguel da Costa Martins Marcelino 2bcaf09734 Revert "TUN-10557: Bump quic-go v0.59.1"
This reverts merge request !1850
2026-06-18 13:30:00 +00:00
Miguel da Costa Martins Marcelino 3315fa6e0f TUN-10630: Fix precheck protocol override
As it stands, cloudflared prechecks are not taking the `protocol` flag into consideration and is instead falling back to the default protocol, which is QUIC. Prechecks should report the protocol cloudflared will use, not the default protocol.
2026-06-18 10:56:53 +00:00
Miguel da Costa Martins Marcelino ad11e67340 chore: Fix warnings
Fixing warnings in cloudflared before making any further changes.
2026-06-16 16:53:56 +00:00
João "Pisco" Fernandes 3a60f8ac0f TUN-10612: Add renovate to cloudflared to update distroless images explicitely 2026-06-15 11:39:47 +01:00
lneto 68620efbce TUN-10557: Bump quic-go v0.59.1
Bumps quic-go to v0.59.1 (chungthuang fork rebased from upstream v0.45 onto
v0.59.1). Upstream removed the `logging` package and replaced its
callback-based ConnectionTracer with the structured `qlog`/`qlogwriter` event
API, which required migrating cloudflared's QUIC metrics collection.

Migrations:

- quic/tracing.go: connTracer no longer fills a logging.ConnectionTracer
  callback struct. It implements qlogwriter.Trace + qlogwriter.Recorder and
  dispatches qlog events (PacketSent, PacketReceived, MetricsUpdated, ...) to
  the collector through RecordEvent. NewClientTracer now returns a function
  compatible with quic.Config.Tracer.

- quic/metrics.go: collector methods take qlog types (qlog.Frame,
  qlog.PacketType, qlog.MetricsUpdated, ...) and plain int64 in place of the
  removed logging.ByteCount/Frame/RTTStats/TransportParameters.

- quic/conversion.go: PacketType, PacketDropReason and PacketLossReason are
  strings upstream rather than numeric iotas, so the converters become
  pass-through allowlists. CongestionState is also a string;
  congestionStateToFloat maps it back to the numeric gauge values cloudflared
  exports.

- quic.Connection/quic.Stream became *quic.Conn/*quic.Stream; updated
  ConnWithCloser, SafeStreamCloser and the connection package accordingly.
  Tests and generated mocks (mocks/mock_quic_connection.go) were adapted to
  the new pointer-based API.

Closes TUN-10557
2026-06-12 07:24:26 +01:00
Miguel da Costa Martins Marcelino 4d95ab73f5 TUN-9251: Publish internal image
Publishing internal image in cloudflared. This allows us to remove the dependency from cloudflare/plat/dockerfiles. In addition, our acceptance tests should now be able to use the latest image instead of relying on a fixed version for testing, which will allow us to detect potential failures earlier.
2026-06-11 12:31:50 +00:00
João "Pisco" Fernandes 57f7d693bb Release 2026.6.0 2026-06-08 19:16:09 +01:00
João "Pisco" Fernandes ccffef1179 TUN-10558: Bump go to v1.24.4, x/crypto to v0.52.0 and google.golang.org/grpc to v1.81.1
Closes TUN-10558
2026-06-08 19:15:35 +01:00
Luis Neto 52519f67e8 TUN-10563: introduce QUICConnection interface
The bump of the QUIC library introduces a cyclic dependency between the connection and quic modules hence it is necessary to break this coupling.

Right now, the connection module depends on the quic module for the datagram v2/v3 and to which a QUIC connection (currently an interface) is passed.

As it is there is no issue however, under the hood, interface is a wrapper around an UDP connection and a QUIC connection meaning this type must be exposed to the quic module since the QUIC Connection will no longer be a interface but a struct.

Given the above, these changes introduce an interface, QUICConnection, with the surface used today in cloudflared and a struct, ConnWithCloser, that implements said interface within the quic module.

Closes TUN-10563
2026-06-01 10:08:38 +01:00
João "Pisco" Fernandes 0e84636de9 Release 2026.5.2 2026-05-27 11:15:36 +01:00
Miguel da Costa Martins Marcelino 4177dd6936 TUN-10391: Avoid using fmt.Println
Avoid using fmt.Println and instead switch to logging pre-checks with the provided logger.
2026-05-26 22:04:54 +00:00
João "Pisco" Fernandes f6f60e1059 Release 2026.5.1 2026-05-25 10:32:09 +01:00
Miguel da Costa Martins Marcelino 4494eee13d TUN-10391: Add precheck integration tests
Adding integration tests for cloudflared pre-checks. This tests pre-check functionality to ensure it is working as expected.
2026-05-22 21:58:48 +00:00
Miguel da Costa Martins Marcelino 905d983d14 TUN-10391: Avoid blocking cloudflared due to logging
Pipes have a finite OS buffer (\~64KB Linux, \~4KB macOS, \~4KB Windows). Since nobody was reading stdout/stderr during the process lifetime, cloudflared would block once the buffer filled up. The post-terminate()/read() could only get whatever fit in the buffer, causing truncated logs.

There was also a race between terminate() and read(): the process might not have flushed its final output yet.

We're also deleting `test_default_only`. Since we changed `edge-ip-version` to auto, this test became redundant.
2026-05-22 18:15:54 +00:00
João "Pisco" Fernandes 168f09cb4c fix: Bump go to 1.26.3 and go.opentelemetry.io/otel and go-jose/v4 to fix CVE's 2026-05-22 17:29:40 +01:00
Miguel da Costa Martins Marcelino 0c9014870a TUN-10511: Revise --edge support for pre-checks
Fixing some bugs with DNS targets. Most importantly, these changes also fix some wrong assumptionsmade when trying to add support for the `--edge` flag:

1. Removes `StaticEdgeDNSResolver` in favor `resolveStaticEdge`. Since --edge does not imply resolving DNS, this fixes that assumption.
2. Adds EdgeAddrs, which allows us to skip DNS probes when set. This fixes the targets in the DNS rows.
3. Added a new `ResolvedTarget` struct, which joins addresses with the respective DNS results. This avoids the brittle logic we had before, where we assumed there were always two groups (one for each region) when running probes. So this not only makes the code more extensible in case we want to add more regions in the future but also adds support for multiple targets supplied via `--edge`.
4. Changes the existing nomenclature, going from calling things `region` to `target`. The term `region` works when resolving production regions (region1 and region2), but becomes misleading when we add the logic for `--edge`.

The end result of these changes is that we now see the correct addresses when you supply targets via `--edge`, while also making the code a bit clearer.
2026-05-14 09:06:02 +00:00
Miguel da Costa Martins Marcelino 31de04f858 TUN-10525: Add prechecks kill switch
Instead of having the  --precheck flag in cloudflared, we allow controlling prechecks via a DNS flag, so we can short-circuit this behavior in case anything goes wrong. Although we don't expect pre-checks to add that much traffic, we should still guarantee that we can stop pre-checks in case something goes wrong.
2026-05-13 18:05:11 +00:00
João "Pisco" Fernandes fbfd76089f fix: Update golang.org/x/net to v0.54.0
Check / check (1.22.x, ubuntu-latest) (push) Failing after 5m15s
Semgrep config / semgrep/ci (push) Failing after 1m19s
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
2026-05-13 13:15:15 +01:00
382 changed files with 19008 additions and 35964 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ RUN apt-get update && \
apt-get install --no-install-recommends --allow-downgrades -y \
build-essential \
git \
go-boring=1.26.2-1 \
go-boring=1.26.4-1 \
libffi-dev \
procps \
python3-dev \
+34
View File
@@ -0,0 +1,34 @@
include:
- local: .ci/commons.gitlab-ci.yml
###########################################################################
### Build and Push Internal Image (commit SHA on master, version on tag) ###
###########################################################################
- component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/build-push-image@~latest
inputs:
stage: release-internal
jobPrefix: internal-image
runOnMR: false
runOnBranches: '^master$'
needs:
- generate-internal-image-version
commentImageRefs: false
runner: vm-linux-x86-4cpu-8gb
EXTRA_DIB_ARGS: "--manifest=.docker-images-internal"
###############################################################################
### Generate Internal Image Version File ###
### Uses `git describe`: version tag on tagged commits, SHA-based on master ###
###############################################################################
generate-internal-image-version:
stage: release-internal
image: $BUILD_IMAGE
rules:
- !reference [.default-rules, run-on-master]
needs:
- ci-image-get-image-ref
script:
- make generate-internal-image-version
artifacts:
paths:
- versions-internal
+1 -1
View File
@@ -5,7 +5,7 @@
runner: linux-x86-8cpu-16gb
stage: build
golangVersion: "boring-1.26"
imageVersion: "3595-779e088c0ec4@sha256:a9825d640211b76915a60071e9bef3f73ad3572ce770c7c7dd36b3dd3687504c"
imageVersion: "3625-1801d52@sha256:9261597bc2d229c997522848260de758567643d58ae1097196ae368db89a1d0f"
CGO_ENABLED: 1
.default-packaging-job: &packaging-job-defaults
+1 -1
View File
@@ -8,7 +8,7 @@ include:
rules:
- !reference [.default-rules, run-always]
tags:
- windows-x86
- canary-windows-x86
cache: {}
##########################################
+8
View File
@@ -0,0 +1,8 @@
images:
- name: cloudflared-daemon
dockerfile: Dockerfile.$ARCH
context: .
version_file: versions-internal
architectures:
- amd64
- arm64
+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
+6 -1
View File
@@ -1,5 +1,5 @@
variables:
GO_VERSION: "1.26.2"
GO_VERSION: "1.26.4"
MAC_GO_VERSION: "go@$GO_VERSION"
WIN_GO_VERSION: "go$GO_VERSION"
GIT_DEPTH: "0"
@@ -63,6 +63,11 @@ include:
#####################################################
- local: .ci/apt-internal.gitlab-ci.yml
#####################################################
########## Release Internal Docker Image ############
#####################################################
- local: .ci/internal-image.gitlab-ci.yml
#####################################################
############## Manual Claude Review #################
#####################################################
+2 -2
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.26.2 AS builder
FROM golang:1.26.4 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-debian13:nonroot
FROM gcr.io/distroless/base-debian13:nonroot@sha256:ab7554b6d07ad354fad31957f8a1a813e65dfb93a8ad160568c79c3f2be6884f
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.26.2 AS builder
FROM golang:1.26.4 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-debian13:nonroot
FROM gcr.io/distroless/base-debian13:nonroot-amd64@sha256:ced0a2b1936b14d5bddc2ee02a807b1586ca6576a967f5b043f4a3301c8a8f6b
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.26.2 AS builder
FROM golang:1.26.4 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-debian13:nonroot-arm64
FROM gcr.io/distroless/base-debian13:nonroot-arm64@sha256:9c1ab6a3dbf9e22827b0be4a314d7cfbe008f922b7ca833ed0e5a63318c6169e
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+4
View File
@@ -159,6 +159,10 @@ container:
generate-docker-version:
echo latest $(VERSION) > versions
.PHONY: generate-internal-image-version
generate-internal-image-version:
echo $(VERSION) > versions-internal
.PHONY: test
test: vet
+41
View File
@@ -1,3 +1,44 @@
2026.7.1
- 2026-07-09 Revert "TUN-10621: Propagate max wait timeout"
2026.7.0
- 2026-07-08 chore: Bump go-chi to version 5.3.1
- 2026-07-08 Revert "TUN-10557: Bump quic-go v0.59.1"
- 2026-07-01 TUN-10621: Test websocket path
- 2026-06-30 ci: add Semgrep OSS scanning workflow
- 2026-06-29 TUN-10621: Propagate max wait timeout
- 2026-06-29 Migrate config renovate.json
- 2026-06-25 chore: Fix renovate
- 2026-06-25 Pin dependencies
- 2026-06-25 Consolidate init-system detection into shared inits package
- 2026-06-24 Fix lint issues
- 2026-06-22 Add OpenRC support
- 2026-06-22 Fix broken installed-service detection in systemd uninstall
- 2026-06-18 TUN-10557: Bump quic-go v0.59.1
2026.6.1
- 2026-06-18 TUN-10630: Fix precheck protocol override
- 2026-06-18 Revert "TUN-10557: Bump quic-go v0.59.1"
- 2026-06-16 chore: Fix warnings
- 2026-06-15 TUN-10612: Add renovate to cloudflared to update distroless images explicitely
- 2026-06-11 TUN-9251: Publish internal image
- 2026-05-26 TUN-10557: Bump quic-go v0.59.1
2026.6.0
- 2026-06-08 TUN-10558: Bump go to v1.24.4, x/crypto to v0.52.0 and google.golang.org/grpc to v1.81.1
- 2026-06-01 TUN-10563: introduce QUICConnection interface
2026.5.2
- 2026-05-26 TUN-10391: Avoid using fmt.Println
2026.5.1
- 2026-05-22 fix: Bump go to 1.26.3 and go.opentelemetry.io/otel and go-jose/v4 to fix CVE's
- 2026-05-22 TUN-10391: Avoid blocking cloudflared due to logging
- 2026-05-22 TUN-10391: Add precheck integration tests
- 2026-05-14 TUN-10511: Revise --edge support for pre-checks
- 2026-05-13 fix: Update golang.org/x/net to v0.54.0
- 2026-05-13 TUN-10525: Add prechecks kill switch
2026.5.0
- 2026-05-08 Bump golang.org/x/net from v0.40.0 to v0.53.0
- 2026-05-07 TUN-10507: Bump go and go-boring to 1.26.2
+2 -3
View File
@@ -17,8 +17,7 @@ import (
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
// This implements the functions for glider proxy (sock5) and the carrier interface
type Websocket struct {
log *zerolog.Logger
isSocks bool
log *zerolog.Logger
}
// NewWSConnection returns a new connection object
@@ -36,7 +35,7 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
ws.log.Err(err).Str(LogFieldOriginURL, options.OriginURL).Msg("failed to connect to origin")
return err
}
defer wsConn.Close()
defer func() { _ = wsConn.Close() }()
stream.Pipe(wsConn, conn, ws.log)
return nil
+22 -16
View File
@@ -2,10 +2,11 @@ package carrier
import (
"context"
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"math/rand"
"math/big"
"testing"
"time"
@@ -23,7 +24,7 @@ import (
func websocketClientTLSConfig(t *testing.T) *tls.Config {
certPool := x509.NewCertPool()
helloCert, err := tlsconfig.GetHelloCertificateX509()
assert.NoError(t, err)
require.NoError(t, err)
certPool.AddCert(helloCert)
assert.NotNil(t, certPool)
return &tls.Config{RootCAs: certPool}
@@ -43,8 +44,8 @@ func TestServe(t *testing.T) {
shutdownC := make(chan struct{})
errC := make(chan error)
listener, err := hello.CreateTLSListener("localhost:1111")
assert.NoError(t, err)
defer listener.Close()
require.NoError(t, err)
defer func() { _ = listener.Close() }()
go func() {
errC <- hello.StartHelloWorldServer(&log, listener, shutdownC)
@@ -56,19 +57,21 @@ func TestServe(t *testing.T) {
assert.NotNil(t, tlsConfig)
d := gws.Dialer{TLSClientConfig: tlsConfig}
conn, resp, err := clientConnect(req, &d)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
for i := 0; i < 1000; i++ {
messageSize := rand.Int()%2048 + 1
clientMessage := make([]byte, messageSize)
// rand.Read always returns len(clientMessage) and a nil error
rand.Read(clientMessage)
messageSize, err := crand.Int(crand.Reader, big.NewInt(2048))
require.NoError(t, err)
clientMessage := make([]byte, int(messageSize.Int64())+1)
_, err = crand.Read(clientMessage)
require.NoError(t, err)
err = conn.WriteMessage(websocket.BinaryFrame, clientMessage)
assert.NoError(t, err)
require.NoError(t, err)
messageType, message, err := conn.ReadMessage()
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, websocket.BinaryFrame, messageType)
assert.Equal(t, clientMessage, message)
}
@@ -97,27 +100,30 @@ func TestWebsocketWrapper(t *testing.T) {
req := testRequest(t, testAddr, nil)
conn, resp, err := clientConnect(req, &d)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
// Websocket now connected to test server so lets check our wrapper
wrapper := cfwebsocket.GorillaConn{Conn: conn}
buf := make([]byte, 100)
wrapper.Write([]byte("abc"))
_, err = wrapper.Write([]byte("abc"))
require.NoError(t, err)
n, err := wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 3)
require.Equal(t, 3, n)
require.Equal(t, "abc", string(buf[:n]))
// Test partial read, read 1 of 3 bytes in one read and the other 2 in another read
wrapper.Write([]byte("abc"))
_, err = wrapper.Write([]byte("abc"))
require.NoError(t, err)
buf = buf[:1]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 1)
require.Equal(t, 1, n)
require.Equal(t, "a", string(buf[:n]))
buf = buf[:cap(buf)]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 2)
require.Equal(t, 2, n)
require.Equal(t, "bc", string(buf[:n]))
}
+57
View File
@@ -1,6 +1,9 @@
package cliutil
import (
"strings"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
FlagLogOutput,
}
}
// LogTable renders lines inside an ASCII table and logs each rendered row.
func LogTable(log *zerolog.Logger, lines []string, title ...string) {
tableTitle := ""
if len(title) > 0 {
tableTitle = title[0]
}
for _, line := range asciiBox(lines, tableTitle, 2) {
if line != "" {
log.Info().Msg(line)
}
}
}
// asciiBox wraps lines in a bordered ASCII box with an optional title row.
func asciiBox(lines []string, title string, padding int) (box []string) {
maxLen := maxLen(lines, title)
spacer := strings.Repeat(" ", padding)
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
box = append(box, border)
if title != "" {
box = append(box, renderBoxLine(centerLine(title, maxLen), maxLen, spacer))
box = append(box, border)
}
for _, line := range lines {
box = append(box, renderBoxLine(line, maxLen, spacer))
}
box = append(box, border)
return
}
// renderBoxLine pads a single line so it fills the box width.
func renderBoxLine(line string, maxLen int, spacer string) string {
return "|" + spacer + line + strings.Repeat(" ", maxLen-len(line)) + spacer + "|"
}
// centerLine pads line evenly so it is centered within width.
func centerLine(line string, width int) string {
padding := width - len(line)
leftPadding := padding / 2
rightPadding := padding - leftPadding
return strings.Repeat(" ", leftPadding) + line + strings.Repeat(" ", rightPadding)
}
// maxLen returns the longest visible line length including the title.
func maxLen(lines []string, title string) int {
max := len(title)
for _, line := range lines {
if len(line) > max {
max = len(line)
}
}
return max
}
+60
View File
@@ -0,0 +1,60 @@
package cliutil
import (
"bytes"
"encoding/json"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogTableWithoutTitle(t *testing.T) {
t.Parallel()
lines := captureTableLogs(t, []string{"first", "second"})
assert.Equal(t, []string{
"+----------+",
"| first |",
"| second |",
"+----------+",
}, lines)
}
func TestLogTableWithTitle(t *testing.T) {
t.Parallel()
lines := captureTableLogs(t, []string{"first", "second"}, "TT")
assert.Equal(t, []string{
"+----------+",
"| TT |",
"+----------+",
"| first |",
"| second |",
"+----------+",
}, lines)
}
func captureTableLogs(t *testing.T, lines []string, title ...string) []string {
t.Helper()
var buf bytes.Buffer
logger := zerolog.New(&buf)
LogTable(&logger, lines, title...)
// nolint: prealloc
var messages []string
for _, line := range bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n")) {
var entry struct {
Message string `json:"message"`
}
require.NoError(t, json.Unmarshal(line, &entry))
messages = append(messages, entry.Message)
}
return messages
}
-3
View File
@@ -126,9 +126,6 @@ const (
// NoPrechecks is the command line flag to skip connectivity pre-checks at startup.
NoPrechecks = "no-prechecks"
// Prechecks is the command line flag to run connectivity pre-checks at startup.
Prechecks = "prechecks"
// LogLevel is the command line flag for the cloudflared logging level
LogLevel = "loglevel"
+39
View File
@@ -0,0 +1,39 @@
// Package inits provides detection of the init system managing the host
// (systemd, OpenRC, or SysV). It is shared by the service installer and the
// auto-updater so that init-system detection lives in a single place.
//
// The functions are safe to call on any GOOS; on non-Linux platforms they
// report that no Linux init system is in use.
package inits
import (
"os"
"runtime"
)
// IsSystemd reports whether the host is managed by systemd.
func IsSystemd() bool {
_, err := os.Stat("/run/systemd/system")
return err == nil
}
// IsOpenRC reports whether the host is managed by OpenRC.
func IsOpenRC() bool {
for _, path := range []string{"/sbin/openrc-run", "/usr/sbin/openrc-run", "/usr/bin/openrc-run"} {
if _, err := os.Stat(path); err == nil {
return true
}
}
return false
}
// IsSysV reports whether the host relies on a SysV-style init system, i.e. a
// Linux host that is managed by neither systemd nor OpenRC. systemd and OpenRC
// keep the service alive themselves, so only SysV needs the process to restart
// itself after an auto-update.
func IsSysV() bool {
if runtime.GOOS != "linux" {
return false
}
return !IsSystemd() && !IsOpenRC()
}
+110 -25
View File
@@ -3,6 +3,7 @@
package main
import (
"errors"
"fmt"
"io"
"os"
@@ -11,6 +12,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/inits"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
@@ -49,13 +51,14 @@ const (
cloudflaredService = "cloudflared.service"
cloudflaredUpdateService = "cloudflared-update.service"
cloudflaredUpdateTimer = "cloudflared-update.timer"
cloudflaredOpenRCService = "cloudflared"
)
var systemdAllTemplates = map[string]ServiceTemplate{
cloudflaredService: {
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
Content: `[Unit]
Description=cloudflared
Description=Cloudflare Tunnel client
After=network-online.target
Wants=network-online.target
@@ -97,12 +100,12 @@ WantedBy=timers.target
var sysvTemplate = ServiceTemplate{
Path: "/etc/init.d/cloudflared",
FileMode: 0755,
FileMode: 0o755,
// nolint: dupword
Content: `#!/bin/sh
# For RedHat and cousins:
# chkconfig: 2345 99 01
# description: cloudflared
# description: Cloudflare Tunnel client
# processname: {{.Path}}
### BEGIN INIT INFO
# Provides: {{.Path}}
@@ -110,8 +113,8 @@ var sysvTemplate = ServiceTemplate{
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: cloudflared
# Description: cloudflared agent
# Short-Description: Cloudflare Tunnel client
# Description: Cloudflare Tunnel client
### END INIT INFO
name=$(basename $(readlink -f $0))
cmd="{{.Path}} --pidfile /var/run/$name.pid {{ range .ExtraArgs }} {{ . }}{{ end }}"
@@ -186,19 +189,52 @@ exit 0
`,
}
var openrcTemplate = ServiceTemplate{
Path: "/etc/init.d/" + cloudflaredOpenRCService,
FileMode: 0o755,
Content: `#!/sbin/openrc-run
description="Cloudflare Tunnel client"
: "${cloudflared_user:=root}"
command="{{.Path}}"
command_args="{{ range .ExtraArgs }} {{ . }}{{ end }}"
command_user="${cloudflared_user}"
pidfile="/run/${RC_SVCNAME}.pid"
output_log="/var/log/${RC_SVCNAME}.log"
error_log="/var/log/${RC_SVCNAME}.err"
# Use OpenRC's supervisor so the tunnel is restarted on failure.
supervisor="supervise-daemon"
respawn_delay=5
respawn_max=0
depend() {
need net
use dns logger
after net firewall
}
`,
}
var openrcConfTemplate = ServiceTemplate{
Path: "/etc/conf.d/" + cloudflaredOpenRCService,
FileMode: 0o644,
Content: `# Configuration for the cloudflared OpenRC service.
# User the cloudflared daemon runs as. Defaults to root.
#cloudflared_user="cloudflared"
`,
}
var noUpdateServiceFlag = &cli.BoolFlag{
Name: "no-update-service",
Usage: "Disable auto-update of the cloudflared linux service, which restarts the server to upgrade for new versions.",
Value: false,
}
func isSystemd() bool {
if _, err := os.Stat("/run/systemd/system"); err == nil {
return true
}
return false
}
func installLinuxService(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
@@ -228,9 +264,12 @@ func installLinuxService(c *cli.Context) error {
templateArgs.ExtraArgs = extraArgs
switch {
case isSystemd():
case inits.IsSystemd():
log.Info().Msgf("Using Systemd")
err = installSystemd(&templateArgs, autoUpdate, log)
case inits.IsOpenRC():
log.Info().Msgf("Using OpenRC")
err = installOpenRC(&templateArgs, autoUpdate)
default:
log.Info().Msgf("Using SysV")
err = installSysv(&templateArgs, autoUpdate, log)
@@ -258,14 +297,11 @@ func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) {
return err == nil && val != ""
}
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
return nil, fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
tunnel: TUNNEL-UUID
credentials-file: CREDENTIALS-FILE
`, src.Source())
return nil, fmt.Errorf("configuration file %s must contain entries for the tunnel to run and its associated credentials (tunnel: TUNNEL-UUID, credentials-file: CREDENTIALS-FILE)", src.Source())
}
if src.Source() != serviceConfigPath {
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
return nil, fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
return nil, fmt.Errorf("possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
}
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
@@ -348,14 +384,40 @@ func installSysv(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolo
return runCommand("service", "cloudflared", "start")
}
func installOpenRC(templateArgs *ServiceTemplateArgs, autoUpdate bool) error {
if autoUpdate {
templateArgs.ExtraArgs = append([]string{"--autoupdate-freq", "24h0m0s"}, templateArgs.ExtraArgs...)
} else {
templateArgs.ExtraArgs = append([]string{"--no-autoupdate"}, templateArgs.ExtraArgs...)
}
if err := openrcConfTemplate.Generate(templateArgs); err != nil {
return fmt.Errorf("error generating OpenRC conf.d template: %w", err)
}
if err := openrcTemplate.Generate(templateArgs); err != nil {
return fmt.Errorf("error generating OpenRC service template: %w", err)
}
if err := runCommand("rc-update", "add", cloudflaredOpenRCService, "default"); err != nil {
return fmt.Errorf("rc-update add %s default: %w", cloudflaredOpenRCService, err)
}
if err := runCommand("rc-service", cloudflaredOpenRCService, "start"); err != nil {
return fmt.Errorf("rc-service %s start: %w", cloudflaredOpenRCService, err)
}
return nil
}
func uninstallLinuxService(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
var err error
switch {
case isSystemd():
case inits.IsSystemd():
log.Info().Msg("Using Systemd")
err = uninstallSystemd(log)
case inits.IsOpenRC():
log.Info().Msg("Using OpenRC")
err = uninstallOpenRC(log)
default:
log.Info().Msg("Using SysV")
err = uninstallSysv(log)
@@ -371,7 +433,11 @@ func uninstallSystemd(log *zerolog.Logger) error {
// Get only the installed services
installedServices := make(map[string]ServiceTemplate)
for serviceName, serviceTemplate := range systemdAllTemplates {
if err := runCommand("systemctl", "list-units", "--all", "|", "grep", serviceName); err == nil {
path, err := serviceTemplate.ResolvePath()
if err != nil {
return fmt.Errorf("error resolving path for service %q: %w", serviceName, err)
}
if _, err := os.Stat(path); err == nil {
installedServices[serviceName] = serviceTemplate
} else {
log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName)
@@ -431,28 +497,47 @@ func uninstallSysv(log *zerolog.Logger) error {
return nil
}
func uninstallOpenRC(log *zerolog.Logger) error {
if err := runCommand("rc-service", cloudflaredOpenRCService, "stop"); err != nil {
log.Warn().Err(err).Msg("could not stop cloudflared OpenRC service, continuing uninstall")
}
if err := runCommand("rc-update", "del", cloudflaredOpenRCService, "default"); err != nil {
log.Warn().Err(err).Msg("could not remove cloudflared from the default runlevel, continuing uninstall")
}
for _, template := range []ServiceTemplate{openrcTemplate, openrcConfTemplate} {
path, err := template.ResolvePath()
if err != nil {
return fmt.Errorf("error resolving OpenRC template path: %w", err)
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("error removing %s: %w", path, err)
}
}
return nil
}
func ensureConfigDirExists(configDir string) error {
ok, err := config.FileExists(configDir)
if !ok && err == nil {
err = os.Mkdir(configDir, 0755)
err = os.Mkdir(configDir, 0o755) //nolint:gosec // config dir must be traversable by a non-root service user
}
return err
}
func copyFile(src, dest string) error {
srcFile, err := os.Open(src)
srcFile, err := os.Open(src) //nolint:gosec // operator-provided service config path
if err != nil {
return err
}
defer srcFile.Close()
defer func() { _ = srcFile.Close() }()
destFile, err := os.Create(dest)
destFile, err := os.Create(dest) //nolint:gosec // operator-provided service config path
if err != nil {
return err
}
ok := false
defer func() {
destFile.Close()
_ = destFile.Close()
if !ok {
_ = os.Remove(dest)
}
+14 -32
View File
@@ -375,17 +375,6 @@ func StartServer(
info.Log(log)
logClientOptions(c, log)
// Run connectivity pre-checks for cloudflared. This runs in a separate
// goroutine, as we want to keep initializing cloudflared while prechecks
// are running.
if c.Bool(cfdflags.Prechecks) && !c.Bool(cfdflags.NoPrechecks) {
resolvedRegion := c.String(cfdflags.Region)
if resolvedRegion == "" && namedTunnel != nil {
resolvedRegion = namedTunnel.Credentials.Endpoint
}
go runPrechecks(c, log, resolvedRegion)
}
// 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()
@@ -428,6 +417,13 @@ func StartServer(
}
connectorID := tunnelConfig.ClientConfig.ConnectorID
// Run connectivity pre-checks for cloudflared. This runs in a separate
// goroutine, as we want to keep initializing cloudflared while prechecks
// are running. Prechecks are controlled via DNS flag for remote kill-switch capability.
if !tunnelConfig.ClientConfig.ConnectionFeaturesSnapshot().SkipPrechecks && !c.Bool(cfdflags.NoPrechecks) {
go runPrechecks(c, log, tunnelConfig.Region)
}
// Disable ICMP packet routing for quick tunnels
if quickTunnelURL != "" {
tunnelConfig.ICMPRouterServer = nil
@@ -541,21 +537,14 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
}
cfg := prechecks.Config{
Region: region,
IPVersion: ipVersion,
}
// Mirror the static/dynamic edge selection from supervisor/supervisor.go:
// when --edge addresses are provided, bypass DNS discovery entirely.
var dnsResolver prechecks.DNSResolver
if edgeAddrs := c.StringSlice(cfdflags.Edge); len(edgeAddrs) > 0 {
dnsResolver = &prechecks.StaticEdgeDNSResolver{Addrs: edgeAddrs, Log: log}
} else {
dnsResolver = &prechecks.EdgeDNSResolver{Log: log}
Region: region,
IPVersion: ipVersion,
EdgeAddrs: c.StringSlice(cfdflags.Edge),
ProtocolOverride: c.String(cfdflags.Protocol),
}
dialers := prechecks.RunDialers{
DNSResolver: dnsResolver,
DNSResolver: &prechecks.EdgeDNSResolver{Log: log},
TCPDialer: &prechecks.EdgeTCPDialer{},
QUICDialer: &prechecks.EdgeQUICDialer{},
ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}},
@@ -563,8 +552,8 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
// Output the human-readable table to console
fmt.Println(report.String())
// Output the human-readable table
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
// Also log structured results for log aggregation
report.LogEvent(log)
@@ -946,13 +935,6 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
Value: false,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: cfdflags.Prechecks,
Usage: "Run connectivity pre-checks at startup.",
EnvVars: []string{"TUNNEL_PRECHECKS"},
Value: false,
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: cfdflags.Metrics,
Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
-1
View File
@@ -252,7 +252,6 @@ func prepareTunnelConfig(
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
NoPrechecks: c.Bool(flags.NoPrechecks),
Prechecks: c.Bool(flags.Prechecks),
OriginDNSService: dnsService,
OriginDialerService: originDialerService,
}
+4 -28
View File
@@ -11,6 +11,7 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/connection"
)
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
if err != nil {
return errors.Wrap(err, "failed to request quick Tunnel")
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
// This will read the entire response into memory so we can print it in case of error
rsp_body, err := io.ReadAll(resp.Body)
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
url = "https://" + url
}
for _, line := range AsciiBox([]string{
cliutil.LogTable(sc.log, []string{
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
url,
}, 2) {
sc.log.Info().Msg(line)
}
})
if !sc.c.IsSet(flags.Protocol) {
_ = sc.c.Set(flags.Protocol, "quic")
@@ -116,26 +115,3 @@ type QuickTunnel struct {
AccountTag string `json:"account_tag"`
Secret []byte `json:"secret"`
}
// Print out the given lines in a nice ASCII box.
func AsciiBox(lines []string, padding int) (box []string) {
maxLen := maxLen(lines)
spacer := strings.Repeat(" ", padding)
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
box = append(box, border)
for _, line := range lines {
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
}
box = append(box, border)
return
}
func maxLen(lines []string) int {
max := 0
for _, line := range lines {
if len(line) > max {
max = len(line)
}
}
return max
}
+5 -15
View File
@@ -16,6 +16,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/cmd/cloudflared/inits"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
@@ -112,10 +113,10 @@ func CheckForUpdate(options updateOptions) (CheckResult, error) {
func encodeWindowsPath(path string) string {
// We do this because Windows allows spaces in directories such as
// Program Files but does not allow these directories to be spaced in batch files.
targetPath := strings.Replace(path, "Program Files (x86)", "PROGRA~2", -1)
targetPath := strings.ReplaceAll(path, "Program Files (x86)", "PROGRA~2")
// This is to do the same in 32 bit systems. We do this second so that the first
// replace is for x86 dirs.
targetPath = strings.Replace(targetPath, "Program Files", "PROGRA~1", -1)
targetPath = strings.ReplaceAll(targetPath, "Program Files", "PROGRA~1")
return targetPath
}
@@ -248,7 +249,7 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
if updateOutcome.Updated {
buildInfo.CloudflaredVersion = updateOutcome.Version
if IsSysV() {
if inits.IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.log.Info().Msg("Restarting service managed by SysV...")
pid, err := a.listeners.StartProcess()
@@ -298,16 +299,5 @@ func wasInstalledFromPackageManager() bool {
}
func isRunningFromTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}
func IsSysV() bool {
if runtime.GOOS != "linux" {
return false
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
return true
return term.IsTerminal(int(os.Stdout.Fd())) // nolint:gosec
}
+4 -31
View File
@@ -1,10 +1,9 @@
import json
import subprocess
from time import sleep
from constants import MANAGEMENT_HOST_NAME
from setup import get_config_from_file
from util import get_tunnel_connector_id
from util import get_tunnel_connector_id, CloudflaredProcess
SINGLE_CASE_TIMEOUT = 600
@@ -83,38 +82,12 @@ class CloudflaredCli:
def __enter__(self):
self.basecmd += ["run"]
self.process = subprocess.Popen(self.basecmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.logger.info(f"Run cmd {self.basecmd}")
return self.process
self.cfd = CloudflaredProcess(self.basecmd, allow_input=False, capture_output=True)
return self.cfd
def __exit__(self, exc_type, exc_value, exc_traceback):
terminate_gracefully(self.process, self.logger, self.basecmd)
self.logger.debug(f"{self.basecmd} logs: {self.process.stderr.read()}")
def terminate_gracefully(process, logger, cmd):
process.terminate()
process_terminated = wait_for_terminate(process)
if not process_terminated:
process.kill()
logger.warning(f"{cmd}: cloudflared did not terminate within wait period. Killing process. logs: \
stdout: {process.stdout.read()}, stderr: {process.stderr.read()}")
def wait_for_terminate(opened_subprocess, attempts=10, poll_interval=1):
"""
wait_for_terminate polls the opened_subprocess every x seconds for a given number of attempts.
It returns true if the subprocess was terminated and false if it didn't.
"""
for _ in range(attempts):
if _is_process_stopped(opened_subprocess):
return True
sleep(poll_interval)
return False
def _is_process_stopped(process):
return process.poll() is not None
self.cfd.cleanup()
def cert_path():
+11
View File
@@ -5,6 +5,17 @@ MAX_LOG_LINES = 50
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
# How long to wait for the cloudflared process to exit after SIGTERM before
# sending SIGKILL.
GRACEFUL_SHUTDOWN_TIMEOUT = 10
# How long to wait for each pipe reader thread to finish after the process
# exits.
READER_THREAD_JOIN_TIMEOUT = 5
# How long to wait for an expected log message to appear before giving up.
LOG_POLL_TIMEOUT = 30
# How often to re-check the accumulated log lines while polling.
LOG_POLL_INTERVAL = 0.5
def protocols():
return ["http2", "quic"]
-19
View File
@@ -17,25 +17,6 @@ class TestEdgeDiscovery:
config["edge-ip-version"] = edge_ip_version
return config
@pytest.mark.parametrize("protocol", protocols())
def test_default_only(self, tmp_path, component_tests_config, protocol):
"""
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():
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):
"""
+10 -7
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python
import json
import os
import time
from constants import MAX_LOG_LINES
from constants import MAX_LOG_LINES, LOG_POLL_INTERVAL, LOG_POLL_TIMEOUT
from util import start_cloudflared, wait_tunnel_ready, send_requests
# Rolling logger rotate log files after 1 MB
@@ -12,12 +13,14 @@ expect_message = "Starting Hello"
def assert_log_to_terminal(cloudflared):
for _ in range(0, MAX_LOG_LINES):
line = cloudflared.stderr.readline()
if not line:
break
if expect_message.encode() in line:
return
# All logs are drained by a background thread into cloudflared.stdout_lines.
# Poll the accumulated lines until the expected message appears.
deadline = time.monotonic() + LOG_POLL_TIMEOUT
while time.monotonic() < deadline:
for line in list(cloudflared.stdout_lines):
if expect_message.encode() in line:
return
time.sleep(LOG_POLL_INTERVAL)
raise Exception(f"terminal log doesn't contain {expect_message}")
+541
View File
@@ -0,0 +1,541 @@
#!/usr/bin/env python
"""
Integration tests for cloudflared connectivity pre-checks (TUN-10391).
Scope
-----
These tests verify the end-to-end behavior of cloudflared pre-checks:
- that the human-readable table written to the log output has the correct
structure and content,
- that structured JSON log lines are emitted with the expected fields, and
- that running the `diag` subcommand against a live tunnel instance produces a
zip archive that contains prechecks.json.
They do NOT cover every failure mode of the precheck logic — those are owned
by the unit tests in prechecks/checker_test.go which use mock dialers.
At the integration level the only reliable way to induce specific failure modes
without real firewall intervention is:
- --edge <unreachable>: StaticEdgeDNSResolver resolves the literal IP
directly (DNS row = PASS), then both QUIC and HTTP/2 probes time out
-> hard fail (both transports blocked).
This does NOT exercise the DNS-failure -> transport-skip path.
DNS failure and Management API failure cannot be triggered via CLI flags alone;
they require network-level intervention outside the component-test harness.
stdout/stderr design
--------------------
The pre-checks table is emitted via cliutil.LogTable, which wraps the content
in an ASCII box and logs each line at Info level through zerolog. zerolog
writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
Popen). We poll a --logfile for the "precheck complete" sentinel before
leaving the `with` block, ensuring the goroutine has finished. We then call
cfd.terminate(). After the `with` block exits, the process is dead and all
output has been captured by CloudflaredProcess's background reader thread. We
read the accumulated lines from cfd.stdout_lines.
Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
+----...----+
| CONNECTIVITY PRE-CHECKS | (centered title)
+----...----+
| COMPONENT TARGET ... | (content rows)
...
+----...----+
"""
import json
import os
import re
import subprocess
import time
import zipfile as zipfilemod
from constants import METRICS_PORT
from util import LOGGER, start_cloudflared, wait_tunnel_ready
# ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
COL_HEADER = "COMPONENT" # first word of the column-header row
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
# Component names (probes.go: componentXxx)
COMP_DNS = "DNS Resolution"
COMP_QUIC = "UDP Connectivity"
COMP_H2 = "TCP Connectivity"
COMP_API = "Cloudflare API"
# Target labels used in the rendered table.
#
# probeRegion() (checker.go:216) always overwrites the Target field of
# whatever CheckResult the inner probe function returns with the regionTarget
# hostname, so QUIC and HTTP/2 rows carry the same region hostname as the
# corresponding DNS row — not the "Port 7844 (QUIC/HTTP2)" strings that
# targetPortQUIC/targetPortHTTP2 define. Those port-label constants are only
# used in the empty-addrs SKIP branch and inside action message strings.
TARGET_API = "api.cloudflare.com:443"
TARGET_REGION1 = "region1.v2.argotunnel.com"
TARGET_REGION2 = "region2.v2.argotunnel.com"
# Details strings (probes.go: detailsXxx)
DETAILS_DNS_RESOLVED = "DNS Resolved successfully"
DETAILS_QUIC_OK = "QUIC connection successful"
DETAILS_HTTP2_OK = "HTTP/2 connection successful"
DETAILS_API_OK = "API is reachable"
DETAILS_QUIC_FAIL = "QUIC connection failed"
DETAILS_HTTP2_FAIL = "HTTP/2 connection is blocked or unreachable"
# Status labels (result.go: xyzStatus)
PASS = "PASS"
FAIL = "FAIL"
SKIP = "SKIP"
# Action prefixes (result.go: renderActions)
PREFIX_ERROR = "ERROR: "
PREFIX_WARNING = "WARNING: "
# Action messages (probes.go: actionXxx)
ACTION_QUIC_BLOCKED = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
ACTION_HTTP2_BLOCKED = "Allow outbound TCP on port 7844."
# Exact summary lines (result.go: summaryLine)
SUMMARY_HEALTHY = "SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol."
SUMMARY_CRITICAL = "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
# structured log constants (result.go)
LOG_MSG_PRECHECK = "precheck"
LOG_MSG_PRECHECK_COMPLETE = "precheck complete"
STATUS_PASS_LOG = "pass"
UNREACHABLE_EDGE = "192.0.2.1:7844"
# cloudflared dial timeout per probe: 5 s, up to 2 retries -> ~15 s total.
PRECHECK_POLL_TIMEOUT_SECS = 15
PRECHECK_POLL_INTERVAL_SECS = 1
# ---------- helpers ----------
def _poll_log_file_for_precheck_complete(log_file: str, timeout: float) -> list[dict]:
"""
Poll a JSON log file until a 'precheck complete' line appears or timeout
expires. Returns all precheck-related log lines found.
cloudflared's --logfile writes one JSON object per line. Polling keeps
the test fast on healthy networks and still tolerates slow CI hosts.
We re-read from the beginning of the file on every poll because the file
is append-only, small, and tracking a byte offset would add complexity with
no meaningful performance benefit for a ~15 s total window.
"""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
lines = _read_precheck_log_lines_from_file(log_file)
if any(l.get("message") == LOG_MSG_PRECHECK_COMPLETE for l in lines):
return lines
time.sleep(PRECHECK_POLL_INTERVAL_SECS)
return _read_precheck_log_lines_from_file(log_file)
def _read_precheck_log_lines_from_file(log_file: str) -> list[dict]:
"""Parse all precheck-related JSON log lines from a --logfile path."""
result = []
try:
with open(log_file, "r") as f:
for raw_line in f:
raw_line = raw_line.strip()
if not raw_line:
continue
try:
obj = json.loads(raw_line)
except json.JSONDecodeError:
continue
msg = obj.get("message") or obj.get("msg", "")
if msg in (LOG_MSG_PRECHECK, LOG_MSG_PRECHECK_COMPLETE):
result.append(obj)
except FileNotFoundError:
pass
return result
# stdout table parse
class TableRow:
"""One data row parsed from the rendered precheck table."""
def __init__(self, component: str, target: str, status: str, details: str):
self.component = component
self.target = target
self.status = status
self.details = details
def __repr__(self):
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
def _strip_log_prefix(line: str) -> str:
"""Remove the zerolog console prefix ('2006-01-02T15:04:05Z LVL ') if present."""
return _LOG_PREFIX_RE.sub("", line, count=1)
def _unbox_line(line: str) -> str:
"""Strip the box border padding from a content line: '| text |' -> 'text'.
Accepts lines that may still carry a zerolog console prefix; the prefix is
removed before the box delimiters are stripped.
"""
msg = _strip_log_prefix(line)
if msg.startswith("|") and msg.endswith("|"):
return msg[1:-1].strip()
return msg.strip()
def _parse_table(stdout: str) -> list[TableRow]:
"""
Parse the data rows from a precheck table in stdout.
The table is now wrapped in an ASCII box by cliutil.LogTable. Each
content line has the form '| <content> |', optionally preceded by a
zerolog console prefix. We strip both the prefix and the box borders
before splitting on two-or-more spaces (text/tabwriter padding=2).
We skip the column-header row and stop at blank lines, SUMMARY, box
border lines, ERROR, or WARNING lines.
"""
rows = []
in_data = False
for raw_line in stdout.splitlines():
msg = _strip_log_prefix(raw_line)
line = _unbox_line(raw_line)
if line.startswith("COMPONENT"):
in_data = True
continue
if not in_data:
continue
if (line == "" or line.startswith("SUMMARY") or BOX_BORDER_RE.match(msg)
or line.startswith("ERROR") or line.startswith("WARNING")):
in_data = False
continue
parts = re.split(r" +", line.rstrip())
if len(parts) >= 3:
rows.append(TableRow(
component=parts[0],
target=parts[1],
status=parts[2],
details=parts[3] if len(parts) >= 4 else "",
))
return rows
def _rows_for(rows: list[TableRow], component: str) -> list[TableRow]:
return [r for r in rows if r.component == component]
# log assertions
def _assert_precheck_summary_log(
log_lines: list[dict],
*,
hard_fail: bool,
suggested_protocol: str | None = None,
):
"""Assert the 'precheck complete' summary log line has the expected fields."""
summary_lines = [l for l in log_lines if l.get("message") == LOG_MSG_PRECHECK_COMPLETE]
assert len(summary_lines) == 1, \
f"Expected exactly one '{LOG_MSG_PRECHECK_COMPLETE}' log line; got {summary_lines}"
summary = summary_lines[0]
assert summary.get("hard_fail") is hard_fail, \
f"Expected hard_fail={hard_fail} in summary log: {summary}"
if suggested_protocol is not None:
assert summary.get("suggested_protocol") == suggested_protocol, \
(f"Expected suggested_protocol={suggested_protocol!r}; "
f"got {summary.get('suggested_protocol')!r}")
# ---------- Tests ----------
class TestPrechecksHappyPath:
"""
On a healthy connection all probes pass. We assert:
- the full table structure (header, column header, separator)
- every row's component, target, status, and details
- no ERROR/WARNING action lines
- the exact summary line
- the structured log summary (hard_fail=false, suggested_protocol=quic)
"""
def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_config):
log_file = str(tmp_path / "cloudflared.log")
config = component_tests_config({"logfile": log_file})
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=["tunnel", "--ha-connections", "1"],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
# Poll the log file for the sentinel before signalling the process.
log_lines = _poll_log_file_for_precheck_complete(
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
)
# Signal shutdown.
cfd.terminate()
# The process is now dead. All output was captured by the background
# reader thread into cfd.stdout_lines (stderr is merged into stdout).
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
# Strip zerolog console prefixes so pattern matching works on raw messages.
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
# ── table structure ──────────────────────────────────────────────────
# zerolog writes to stderr which is merged into stdout by the harness.
# The table is wrapped in an ASCII box by cliutil.LogTable.
assert BOX_TITLE in messages, \
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
assert COL_HEADER in messages, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert BOX_BORDER_RE.search(messages), \
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
assert len(rows) == 7, \
f"Expected 7 rows (2 DNS + 2 QUIC + 2 HTTP/2 + 1 API); got {len(rows)}: {rows}"
dns_rows = _rows_for(rows, COMP_DNS)
assert len(dns_rows) == 2, f"Expected 2 DNS rows; got {dns_rows}"
assert dns_rows[0].target == TARGET_REGION1
assert dns_rows[1].target == TARGET_REGION2
for r in dns_rows:
assert r.status == PASS, f"DNS row not PASS: {r}"
assert r.details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {r}"
quic_rows = _rows_for(rows, COMP_QUIC)
assert len(quic_rows) == 2, f"Expected 2 QUIC rows; got {quic_rows}"
assert quic_rows[0].target == TARGET_REGION1, f"QUIC row[0] target wrong: {quic_rows[0]}"
assert quic_rows[1].target == TARGET_REGION2, f"QUIC row[1] target wrong: {quic_rows[1]}"
for r in quic_rows:
assert r.status == PASS, f"QUIC row not PASS: {r}"
assert r.details == DETAILS_QUIC_OK, f"QUIC row details wrong: {r}"
h2_rows = _rows_for(rows, COMP_H2)
assert len(h2_rows) == 2, f"Expected 2 HTTP/2 rows; got {h2_rows}"
assert h2_rows[0].target == TARGET_REGION1, f"HTTP/2 row[0] target wrong: {h2_rows[0]}"
assert h2_rows[1].target == TARGET_REGION2, f"HTTP/2 row[1] target wrong: {h2_rows[1]}"
for r in h2_rows:
assert r.status == PASS, f"HTTP/2 row not PASS: {r}"
assert r.details == DETAILS_HTTP2_OK, f"HTTP/2 row details wrong: {r}"
api_rows = _rows_for(rows, COMP_API)
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
# ── no action lines ──────────────────────────────────────────────────
assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
# ── summary line ─────────────────────────────────────────────────────
assert SUMMARY_HEALTHY in messages, \
f"Expected healthy summary;\ngot:\n{stdout}"
# ── structured log ───────────────────────────────────────────────────
assert len(log_lines) > 0, \
"Expected at least one structured precheck log line in log file"
for line in log_lines:
if line.get("message") == LOG_MSG_PRECHECK:
assert line.get("status") == STATUS_PASS_LOG, \
f"Expected status=pass in precheck log line: {line}"
_assert_precheck_summary_log(log_lines, hard_fail=False, suggested_protocol="quic")
class TestPrechecksHardFail:
"""
When --edge points at an unreachable IP, StaticEdgeDNSResolver resolves
the literal address directly (DNS row = PASS), but both transport probes
time out -> hard fail. We assert:
- the full table structure
- DNS row: PASS (the literal IP was resolved)
- QUIC row: FAIL with correct details + ERROR action
- HTTP/2 row: FAIL with correct details + ERROR action
- API row: PASS (api.cloudflare.com:443 is independently reachable)
- the exact critical summary line
- the structured log summary (hard_fail=true)
This test does NOT call wait_tunnel_ready because the tunnel will not
connect to the unreachable address.
"""
def test_prechecks_hard_fail_when_edge_unreachable(self, tmp_path, component_tests_config):
log_file = str(tmp_path / "cloudflared.log")
config = component_tests_config({"logfile": log_file})
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=[
"tunnel",
"--ha-connections", "1",
"--edge", UNREACHABLE_EDGE,
],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
log_lines = _poll_log_file_for_precheck_complete(
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
)
cfd.terminate()
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
# Strip zerolog console prefixes so pattern matching works on raw messages.
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
# ── table structure ──────────────────────────────────────────────────
# zerolog writes to stderr which is merged into stdout by the harness.
# The table is wrapped in an ASCII box by cliutil.LogTable.
assert BOX_TITLE in messages, \
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
assert COL_HEADER in messages, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert BOX_BORDER_RE.search(messages), \
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
assert len(rows) == 4, \
f"Expected 4 rows (1 DNS + 1 QUIC + 1 HTTP/2 + 1 API); got {len(rows)}: {rows}"
dns_rows = _rows_for(rows, COMP_DNS)
assert len(dns_rows) == 1, f"Expected 1 DNS row; got {dns_rows}"
assert dns_rows[0].target == UNREACHABLE_EDGE
assert dns_rows[0].status == PASS, f"DNS row not PASS: {dns_rows[0]}"
assert dns_rows[0].details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {dns_rows[0]}"
quic_rows = _rows_for(rows, COMP_QUIC)
assert len(quic_rows) == 1, f"Expected 1 QUIC row; got {quic_rows}"
assert quic_rows[0].target == UNREACHABLE_EDGE, f"QUIC row target wrong: {quic_rows[0]}"
assert quic_rows[0].status == FAIL, f"QUIC row not FAIL: {quic_rows[0]}"
assert quic_rows[0].details == DETAILS_QUIC_FAIL, f"QUIC row details wrong: {quic_rows[0]}"
h2_rows = _rows_for(rows, COMP_H2)
assert len(h2_rows) == 1, f"Expected 1 HTTP/2 row; got {h2_rows}"
assert h2_rows[0].target == UNREACHABLE_EDGE, f"HTTP/2 row target wrong: {h2_rows[0]}"
assert h2_rows[0].status == FAIL, f"HTTP/2 row not FAIL: {h2_rows[0]}"
assert h2_rows[0].details == DETAILS_HTTP2_FAIL, f"HTTP/2 row details wrong: {h2_rows[0]}"
api_rows = _rows_for(rows, COMP_API)
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in messages, \
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in messages, \
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
assert SUMMARY_CRITICAL in messages, \
f"Expected critical summary;\ngot:\n{stdout}"
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
class TestPreChecksDiag:
"""
Verify that `cloudflared tunnel diag` includes prechecks.json in the
diagnostic zip archive produced against a live tunnel instance.
The precheck job in diagnostic.go is gated on noDiagNetwork; we do NOT
pass --no-diag-network so prechecks.json must be present. We skip the
heavier collectors (logs, metrics, system, runtime) to keep the test fast.
The diag subcommand writes the zip to its current working directory. We
run it with cwd=tmp_path so the archive lands there and is cleaned up
automatically by pytest. We resolve config.cloudflared_binary to an
absolute path before changing cwd, because the binary path may be relative
to the original working directory.
"""
def test_diag_contains_prechecks_json(self, tmp_path, component_tests_config):
config = component_tests_config()
binary = os.path.abspath(config.cloudflared_binary)
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=["tunnel", "--ha-connections", "1"],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
# Run the diag subcommand as a one-shot process against the
# already-running instance. We skip log/metrics/system/runtime
# collectors; the network collector (which runs prechecks) is left
# enabled.
diag_result = subprocess.run(
[
binary,
"tunnel",
"diag",
"--metrics", f"localhost:{METRICS_PORT}",
"--no-diag-logs",
"--no-diag-metrics",
"--no-diag-system",
"--no-diag-runtime",
],
cwd=str(tmp_path),
capture_output=True,
timeout=60,
)
cfd.terminate()
diag_stdout = diag_result.stdout.decode(errors="replace")
diag_stderr = diag_result.stderr.decode(errors="replace")
LOGGER.debug(f"[diag] stdout:\n{diag_stdout}")
LOGGER.debug(f"[diag] stderr:\n{diag_stderr}")
assert diag_result.returncode == 0, (
f"cloudflared tunnel diag exited with code {diag_result.returncode}\n"
f"stdout:\n{diag_stdout}\nstderr:\n{diag_stderr}"
)
# Locate the zip file written to tmp_path by the diag command.
zip_files = list(tmp_path.glob("cloudflared-diag-*.zip"))
assert len(zip_files) == 1, \
f"Expected exactly one cloudflared-diag-*.zip in {tmp_path}; found {zip_files}"
zip_path = zip_files[0]
with zipfilemod.ZipFile(zip_path) as zf:
names = zf.namelist()
LOGGER.debug(f"[diag] zip contents: {names}")
assert "prechecks.json" in names, \
f"Expected prechecks.json in diag zip; got: {names}"
# Must be valid JSON containing at least the RunID field that
# prechecks.Run() always sets.
with zf.open("prechecks.json") as fh:
data = json.load(fh)
assert "RunID" in data, \
f"Expected RunID key in prechecks.json; got keys: {list(data.keys())}"
+62 -8
View File
@@ -2,6 +2,7 @@ import logging
import os
import platform
import subprocess
import threading
from contextlib import contextmanager
from time import sleep
import sys
@@ -12,7 +13,65 @@ import requests
import yaml
from retrying import retry
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS, GRACEFUL_SHUTDOWN_TIMEOUT, READER_THREAD_JOIN_TIMEOUT
class CloudflaredProcess:
"""
Wrapper around a Popen process that continuously drains stdout and stderr
in background threads to prevent OS pipe buffers from filling up and
blocking the child process. Captured output is logged when the process
is cleaned up.
"""
def __init__(self, cmd, allow_input, capture_output):
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
stdin = subprocess.PIPE if allow_input else None
self.process = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=subprocess.STDOUT)
self._capture_output = capture_output
self._stdout_lines = []
self._threads = []
if capture_output:
self._threads.append(self._start_reader(self.process.stdout, self._stdout_lines))
@staticmethod
def _start_reader(pipe, sink):
def _drain():
for line in pipe:
sink.append(line)
pipe.close()
t = threading.Thread(target=_drain, daemon=True)
t.start()
return t
def terminate(self):
"""Terminate the process if it is still running."""
if self.process.poll() is None:
self.process.terminate()
def cleanup(self):
"""Terminate, wait for exit, join reader threads, and log output."""
self.terminate()
try:
self.process.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
for t in self._threads:
t.join(timeout=READER_THREAD_JOIN_TIMEOUT)
if self._capture_output:
stdout = b"".join(self._stdout_lines).decode("utf-8", errors="replace")
if stdout:
LOGGER.info(f"cloudflared stdout:\n{stdout}")
@property
def stdout_lines(self):
return self._stdout_lines
# Proxy common Popen attributes so callers can still use the wrapper
# as if it were a Popen (e.g. send_signal, stdin, pid, returncode).
def __getattr__(self, name):
return getattr(self.process, name)
def configure_logger():
logger = logging.getLogger(__name__)
@@ -75,20 +134,15 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
LOGGER.info(f"Run cmd {cmd} with config {config}")
return cmd
@contextmanager
def run_cloudflared_background(cmd, allow_input, capture_output):
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
stdin = subprocess.PIPE if allow_input else None
cfd = None
try:
cfd = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=output)
cfd = CloudflaredProcess(cmd, allow_input, capture_output)
yield cfd
finally:
if cfd:
cfd.terminate()
if capture_output:
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
cfd.cleanup()
def get_quicktunnel_url():
+3 -1
View File
@@ -84,7 +84,7 @@ type TunnelToken struct {
}
func (t TunnelToken) Credentials() Credentials {
// nolint: gosimple
// nolint: staticcheck
return Credentials{
AccountTag: t.AccountTag,
TunnelSecret: t.TunnelSecret,
@@ -122,6 +122,7 @@ const (
// ShouldFlush returns whether this kind of connection should actively flush data
func (t Type) shouldFlush() bool {
// nolint: exhaustive
switch t {
case TypeWebsocket, TypeTCP, TypeControlStream:
return true
@@ -131,6 +132,7 @@ func (t Type) shouldFlush() bool {
}
func (t Type) String() string {
// nolint: exhaustive
switch t {
case TypeWebsocket:
return "websocket"
+2 -2
View File
@@ -146,8 +146,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
case <-wsCtx.Done():
case <-r.Context().Done():
}
readPipe.Close()
writePipe.Close()
_ = readPipe.Close()
_ = writePipe.Close()
}()
originConn := &echoPipe{reader: readPipe, writer: writePipe}
+3 -18
View File
@@ -13,6 +13,7 @@ import (
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection/dialopts"
cfdquic "github.com/cloudflare/cloudflared/quic"
)
var (
@@ -29,7 +30,7 @@ func DialQuic(
connIndex uint8,
logger *zerolog.Logger,
opts dialopts.DialOpts,
) (quic.Connection, error) {
) (cfdquic.QUICConnection, error) {
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, opts, logger)
if err != nil {
return nil, err
@@ -43,11 +44,7 @@ func DialQuic(
}
// wrap the session, so that the UDPConn is closed after session is closed.
conn = &wrapCloseableConnQuicConnection{
conn,
udpConn,
}
return conn, nil
return cfdquic.NewQUICConnection(conn, udpConn)
}
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, opts dialopts.DialOpts, logger *zerolog.Logger) (*net.UDPConn, error) {
@@ -96,15 +93,3 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.Add
return udpConn, err
}
type wrapCloseableConnQuicConnection struct {
quic.Connection
udpConn *net.UDPConn
}
func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error {
err := w.Connection.CloseWithError(errorCode, reason)
_ = w.udpConn.Close()
return err
}
+4 -4
View File
@@ -41,7 +41,7 @@ const (
// quicConnection represents the type that facilitates Proxying via QUIC streams.
type quicConnection struct {
conn quic.Connection
conn cfdquic.QUICConnection
logger *zerolog.Logger
orchestrator Orchestrator
datagramHandler DatagramSessionHandler
@@ -54,10 +54,10 @@ type quicConnection struct {
gracePeriod time.Duration
}
// NewTunnelConnection takes a [quic.Connection] to wrap it for use with cloudflared application logic.
// NewTunnelConnection takes a [cfdquic.QUICConnection] to wrap it for use with cloudflared application logic.
func NewTunnelConnection(
ctx context.Context,
conn quic.Connection,
conn cfdquic.QUICConnection,
connIndex uint8,
orchestrator Orchestrator,
datagramSessionHandler DatagramSessionHandler,
@@ -169,7 +169,7 @@ func (q *quicConnection) acceptStream(ctx context.Context) error {
func (q *quicConnection) runStream(quicStream quic.Stream) {
ctx := quicStream.Context()
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
defer stream.Close()
defer func() { _ = stream.Close() }()
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
// code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream.
+6 -11
View File
@@ -8,9 +8,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
pkgerrors "github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -24,7 +22,6 @@ import (
"github.com/cloudflare/cloudflared/packet"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tracing"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
)
@@ -34,20 +31,18 @@ const (
demuxChanCapacity = 16
)
var (
errInvalidDestinationIP = errors.New("unable to parse destination IP")
)
var errInvalidDestinationIP = pkgerrors.New("unable to parse destination IP")
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
// connection streams.
type DatagramSessionHandler interface {
Serve(context.Context) error
pogs.SessionManager
tunnelpogs.SessionManager
}
type datagramV2Connection struct {
conn quic.Connection
conn cfdquic.QUICConnection
index uint8
// sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer
@@ -69,7 +64,7 @@ type datagramV2Connection struct {
}
func NewDatagramV2Connection(ctx context.Context,
conn quic.Connection,
conn cfdquic.QUICConnection,
originDialer ingress.OriginUDPDialer,
icmpRouter ingress.ICMPRouter,
index uint8,
@@ -166,7 +161,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
if err != nil {
originProxy.Close()
_ = originProxy.Close()
log.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).Msgf("Failed to register udp session")
tracing.EndWithErrorStatus(registerSpan, err)
q.flowLimiter.Release()
@@ -229,7 +224,7 @@ func (q *datagramV2Connection) closeUDPSession(ctx context.Context, sessionID uu
}
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
defer stream.Close()
defer func() { _ = stream.Close() }()
rpcClientStream, err := rpcquic.NewSessionClient(ctx, stream, q.rpcTimeout)
if err != nil {
// Log this at debug because this is not an error if session was closed due to lost connection
+9 -9
View File
@@ -7,12 +7,12 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/management"
cfdquic "github.com/cloudflare/cloudflared/quic/v3"
cfdquic "github.com/cloudflare/cloudflared/quic"
cfdquicv3 "github.com/cloudflare/cloudflared/quic/v3"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
@@ -22,20 +22,20 @@ var (
)
type datagramV3Connection struct {
conn quic.Connection
conn cfdquic.QUICConnection
index uint8
// datagramMuxer mux/demux datagrams from quic connection
datagramMuxer cfdquic.DatagramConn
metrics cfdquic.Metrics
datagramMuxer cfdquicv3.DatagramConn
metrics cfdquicv3.Metrics
logger *zerolog.Logger
}
func NewDatagramV3Connection(ctx context.Context,
conn quic.Connection,
sessionManager cfdquic.SessionManager,
conn cfdquic.QUICConnection,
sessionManager cfdquicv3.SessionManager,
icmpRouter ingress.ICMPRouter,
index uint8,
metrics cfdquic.Metrics,
metrics cfdquicv3.Metrics,
logger *zerolog.Logger,
) DatagramSessionHandler {
log := logger.
@@ -43,7 +43,7 @@ func NewDatagramV3Connection(ctx context.Context,
Int(management.EventTypeKey, int(management.UDP)).
Uint8(LogFieldConnIndex, index).
Logger()
datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
datagramMuxer := cfdquicv3.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
return &datagramV3Connection{
conn,
+4
View File
@@ -42,6 +42,10 @@ type FeatureSnapshot struct {
// We provide the list of features since we need it to send in the ConnectionOptions during connection
// registrations.
FeaturesList []string
// SkipPrechecks indicates when to skip connectivity pre-checks at startup.
// Controlled via DNS TXT record to allow remote kill-switch in case of issues.
SkipPrechecks bool
}
type PostQuantumMode uint8
+9 -1
View File
@@ -24,6 +24,7 @@ const (
type featuresRecord struct {
DatagramV3Percentage uint32 `json:"dv3_2"`
SkipPrechecks bool `json:"skip_prechecks"`
// DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291
// DatagramV3Percentage uint32 `json:"dv3_1"` // Removed in TUN-9883
@@ -89,6 +90,7 @@ func (fs *featureSelector) Snapshot() FeatureSnapshot {
PostQuantum: fs.postQuantumMode(),
DatagramVersion: fs.datagramVersion(),
FeaturesList: fs.clientFeatures(),
SkipPrechecks: fs.prechecksSkip(),
}
}
@@ -121,6 +123,12 @@ func (fs *featureSelector) datagramVersion() DatagramVersion {
return DatagramV2
}
// prechecksSkip returns whether prechecks are enabled via DNS flag.
// Defaults to false if not set in the DNS TXT record.
func (fs *featureSelector) prechecksSkip() bool {
return fs.remoteFeatures.SkipPrechecks
}
// clientFeatures will return the list of currently available features that cloudflared should provide to the edge.
func (fs *featureSelector) clientFeatures() []string {
// Evaluate any remote features along with static feature list to construct the list of features
@@ -186,7 +194,7 @@ func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) {
}
if len(records) == 0 {
return nil, fmt.Errorf("No TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
return nil, fmt.Errorf("no TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
}
return []byte(records[0]), nil
+21 -21
View File
@@ -10,9 +10,9 @@ require (
github.com/fortytw2/leaktest v1.3.0
github.com/fsnotify/fsnotify v1.4.9
github.com/getsentry/sentry-go v0.43.0
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/chi/v5 v5.3.1
github.com/go-chi/cors v1.2.1
github.com/go-jose/go-jose/v4 v4.1.3
github.com/go-jose/go-jose/v4 v4.1.4
github.com/gobwas/ws v1.2.1
github.com/google/gopacket v1.1.19
github.com/google/uuid v1.6.0
@@ -29,19 +29,19 @@ require (
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.40.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.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.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0
go.uber.org/automaxprocs v1.6.0
go.uber.org/mock v0.5.1
golang.org/x/crypto v0.50.0
golang.org/x/net v0.53.0
golang.org/x/crypto v0.52.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.43.0
golang.org/x/term v0.42.0
google.golang.org/protobuf v1.36.6
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
nhooyr.io/websocket v1.8.7
@@ -69,7 +69,7 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // 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-gateway/v2 v2.28.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
@@ -89,15 +89,15 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/grpc v1.72.2 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.81.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+46 -44
View File
@@ -54,14 +54,14 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
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.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/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=
@@ -111,8 +111,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
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-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
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=
@@ -220,21 +220,21 @@ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbE
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.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.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 v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4=
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.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -245,18 +245,18 @@ golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
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/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
@@ -269,31 +269,33 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+1 -1
View File
@@ -43,7 +43,7 @@ func (tc *tcpConnection) Stream(_ context.Context, tunnelConn io.ReadWriter, _ *
func (tc *tcpConnection) Write(b []byte) (int, error) {
if tc.writeTimeout > 0 {
if err := tc.Conn.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
if err := tc.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
tc.logger.Err(err).Msg("Error setting write deadline for TCP connection")
}
}
+23 -62
View File
@@ -13,7 +13,6 @@ import (
"time"
"github.com/gobwas/ws/wsutil"
gorillaWS "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/proxy"
@@ -61,7 +60,7 @@ func TestStreamTCPConnection(t *testing.T) {
})
errGroup.Go(func() error {
echoTCPOrigin(t, originConn)
originConn.Close()
_ = originConn.Close()
return nil
})
@@ -88,7 +87,7 @@ func TestDefaultStreamWSOverTCPConnection(t *testing.T) {
})
errGroup.Go(func() error {
echoTCPOrigin(t, originConn)
originConn.Close()
_ = originConn.Close()
return nil
})
@@ -117,14 +116,14 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
for _, status := range statusCodes {
handler := func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, []byte(sendMessage), body)
assert.NoError(t, err)
assert.Equal(t, []byte(sendMessage), body)
require.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
assert.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
w.Header().Set(echoHeaderName, echoHeaderReturnValue)
w.WriteHeader(status)
w.Write([]byte(echoMessage))
_, _ = w.Write([]byte(echoMessage))
}
origin := httptest.NewServer(http.HandlerFunc(handler))
defer origin.Close()
@@ -156,7 +155,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
errGroup.Go(func() error {
wsForwarderInConn, err := wsForwarderListener.Accept()
require.NoError(t, err)
defer wsForwarderInConn.Close()
defer func() { _ = wsForwarderInConn.Close() }()
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, TestLogger)
return nil
@@ -171,20 +170,22 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
// Request URL doesn't matter because the transport is using eyeballDialer to connectq
req, err := http.NewRequestWithContext(ctx, "GET", "http://test-socks-stream.com", bytes.NewBuffer([]byte(sendMessage)))
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = req.Body.Close() }()
req.Header.Set(echoHeaderName, echoHeaderIncomingValue)
resp, err := transport.RoundTrip(req)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, status, resp.StatusCode)
require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, []byte(echoMessage), body)
wsForwarderOutConn.Close()
edgeConn.Close()
tcpOverWSConn.Close()
_ = wsForwarderOutConn.Close()
_ = edgeConn.Close()
_ = tcpOverWSConn.Close()
require.NoError(t, errGroup.Wait())
}
@@ -205,7 +206,7 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
go func() {
time.Sleep(time.Millisecond * 10)
// Simulate losing connection to origin
originConn.Close()
_ = originConn.Close()
}()
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
tcpOverWSConn.Stream(ctx, eyeballConn, TestLogger)
@@ -221,11 +222,13 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
for i := 0; i < 50; i++ {
eyeballConn, edgeConn := net.Pipe()
req, err := http.NewRequestWithContext(ctx, http.MethodConnect, server.URL, edgeConn)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = req.Body.Close() }()
resp, err := client.Transport.RoundTrip(req)
assert.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer func() { _ = resp.Body.Close() }()
errGroup.Go(func() error {
for {
@@ -261,60 +264,18 @@ func echoWSEyeball(t *testing.T, conn net.Conn) {
assert.NoError(t, conn.Close())
}()
if !assert.NoError(t, wsutil.WriteClientBinary(conn, testMessage)) {
return
}
require.NoError(t, wsutil.WriteClientBinary(conn, testMessage))
readMsg, err := wsutil.ReadServerBinary(conn)
if !assert.NoError(t, err) {
return
}
require.NoError(t, err)
assert.Equal(t, testResponse, readMsg)
}
func echoWSOrigin(t *testing.T, expectMessages bool) *httptest.Server {
var upgrader = gorillaWS.Upgrader{
ReadBufferSize: 10,
WriteBufferSize: 10,
}
ws := func(w http.ResponseWriter, r *http.Request) {
header := make(http.Header)
for k, vs := range r.Header {
if k == "Test-Cloudflared-Echo" {
header[k] = vs
}
}
conn, err := upgrader.Upgrade(w, r, header)
require.NoError(t, err)
defer conn.Close()
sawMessage := false
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
if expectMessages && !sawMessage {
t.Errorf("unexpected error: %v", err)
}
return
}
assert.Equal(t, testMessage, p)
sawMessage = true
if err := conn.WriteMessage(messageType, testResponse); err != nil {
return
}
}
}
// NewTLSServer starts the server in another thread
return httptest.NewTLSServer(http.HandlerFunc(ws))
}
func echoTCPOrigin(t *testing.T, conn net.Conn) {
readBuffer := make([]byte, len(testMessage))
_, err := conn.Read(readBuffer)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, testMessage, readBuffer)
+7 -6
View File
@@ -17,12 +17,13 @@ import (
reflect "reflect"
time "time"
quic "github.com/quic-go/quic-go"
quic0 "github.com/quic-go/quic-go"
zerolog "github.com/rs/zerolog"
gomock "go.uber.org/mock/gomock"
dialopts "github.com/cloudflare/cloudflared/connection/dialopts"
allregions "github.com/cloudflare/cloudflared/edgediscovery/allregions"
quic "github.com/cloudflare/cloudflared/quic"
)
// MockDNSResolver is a mock of DNSResolver interface.
@@ -176,10 +177,10 @@ func (m *MockQUICDialer) EXPECT() *MockQUICDialerMockRecorder {
}
// DialQuic mocks base method.
func (m *MockQUICDialer) DialQuic(ctx context.Context, quicConfig *quic.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.Connection, error) {
func (m *MockQUICDialer) DialQuic(ctx context.Context, quicConfig *quic0.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.QUICConnection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DialQuic", ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
ret0, _ := ret[0].(quic.Connection)
ret0, _ := ret[0].(quic.QUICConnection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -197,19 +198,19 @@ type MockQUICDialerDialQuicCall struct {
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICDialerDialQuicCall) Return(arg0 quic.Connection, arg1 error) *MockQUICDialerDialQuicCall {
func (c *MockQUICDialerDialQuicCall) Return(arg0 quic.QUICConnection, arg1 error) *MockQUICDialerDialQuicCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICDialerDialQuicCall) Do(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
func (c *MockQUICDialerDialQuicCall) Do(f func(context.Context, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, error)) *MockQUICDialerDialQuicCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, error)) *MockQUICDialerDialQuicCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
+129 -88
View File
@@ -30,16 +30,17 @@ type RunDialers struct {
ManagementDialer ManagementDialer
}
// TransportResults holds the per-region results for each transport probe type.
// Each slice has one entry per DNS-resolved region, in the same order as dnsResults.
// TransportResults holds the per-target results for each transport probe type.
// Each slice has one entry per resolved target group, in the same order as the
// target labels slice.
type TransportResults struct {
QUIC []CheckResult // one per region
HTTP2 []CheckResult // one per region
ManagementAPI CheckResult // single target, no regions
QUIC []CheckResult // one per target group
HTTP2 []CheckResult // one per target group
ManagementAPI CheckResult // single target, no groups
}
// Collect returns all results as a slice in a consistent order for reporting:
// all QUIC rows first (one per region), then all HTTP2 rows, then Management API.
// all QUIC rows first (one per target), then all HTTP2 rows, then Management API.
func (tr TransportResults) Collect() []CheckResult {
results := make([]CheckResult, 0, len(tr.QUIC)+len(tr.HTTP2)+1)
results = append(results, tr.QUIC...)
@@ -50,8 +51,11 @@ func (tr TransportResults) Collect() []CheckResult {
// Run executes the following connectivity pre-checks:
//
// 1. DNS resolution (sequential transport probes depend on its output).
// 2. QUIC, HTTP/2, and Management API probes run concurrently.
// 1. Edge address resolution — either DNS-based SRV discovery (normal path)
// or direct resolution of --edge addresses (static path). The static path
// skips DNS probe rows entirely since there are no SRV records to validate.
// 2. QUIC, HTTP/2, and Management API probes run concurrently against the
// resolved addresses.
//
// Each failed probe is retried up to maxRetries times with exponential backoff.
// The suite is bounded by cfg.Timeout (defaultTimeout if zero).
@@ -64,19 +68,39 @@ func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, ru
ctx, cancel := context.WithTimeout(ctx, cfg.Timeout)
defer cancel()
// Build TLS configs once per protocol
// Build TLS configs once per protocol.
quicTLSConfig, quicTLSErr := probeTLSConfig(caCert, connection.QUIC)
http2TLSConfig, http2TLSErr := probeTLSConfig(caCert, connection.HTTP2)
// 1) DNS must complete before transport probes know which addresses to dial.
addrGroups, dnsResults := runDNSProbe(ctx, runDialers.DNSResolver, cfg.Region)
// 1) Resolve edge addresses. Each ResolvedTarget bundles its addr group
// with the DNS CheckResult that labels it, keeping the two in sync.
var resolvedTargets []ResolvedTarget
if len(cfg.EdgeAddrs) > 0 {
// Static path: explicit --edge addresses, one ResolvedTarget per addr.
resolvedTargets = resolveStaticEdge(cfg.EdgeAddrs, log)
} else {
// Normal path: SRV-based discovery; DNS rows carry Pass or Fail status.
resolvedTargets = runDNSProbe(ctx, runDialers.DNSResolver, cfg.Region)
}
dnsOK := !slices.ContainsFunc(dnsResults, func(r CheckResult) bool {
return r.ProbeStatus != Pass
// Extract parallel slices for the transport probe layer.
// nolint:prealloc // False positive. The linter is confused by the append used when producing Report.Results
dnsResults := make([]CheckResult, len(resolvedTargets))
perGroupAddrs := make([][]*allregions.EdgeAddr, len(resolvedTargets))
targetLabels := make([]string, len(resolvedTargets))
for i, rt := range resolvedTargets {
dnsResults[i] = rt.DNSResult
perGroupAddrs[i] = rt.Addrs
targetLabels[i] = rt.DNSResult.Target
}
// dnsOK is true when at least one target has addresses to probe.
dnsOK := slices.ContainsFunc(resolvedTargets, func(r ResolvedTarget) bool {
return len(r.Addrs) > 0
})
// 2) Run probes concurrently. Each probe type gets its own buffered channel —
// one send, one receive, no routing or name-parsing required.
// 2) Run transport probes concurrently. Each probe type gets its own
// buffered channel — one send, one receive, no routing required.
var results TransportResults
mgmtCh := make(chan CheckResult)
@@ -85,12 +109,12 @@ func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, ru
}()
if !dnsOK {
// DNS failed: emit one skip row per region so the table stays consistent.
results.QUIC = skipResultsForRegions(dnsResults, ProbeTypeQUIC, componentUDPConnectivity)
results.HTTP2 = skipResultsForRegions(dnsResults, ProbeTypeHTTP2, componentTCPConnectivity)
// No addresses available: emit one skip row per target so the table
// stays consistent with the DNS rows above.
results.QUIC = skipResultsForTargets(dnsResults, ProbeTypeQUIC, componentUDPConnectivity)
results.HTTP2 = skipResultsForTargets(dnsResults, ProbeTypeHTTP2, componentTCPConnectivity)
} else {
perRegionAddrs := addrsByRegion(addrGroups, cfg.IPVersion)
regionTargets := dnsTargets(dnsResults)
filteredAddrs := addrsByGroup(perGroupAddrs, cfg.IPVersion)
quicCh := make(chan []CheckResult, 1)
http2Ch := make(chan []CheckResult, 1)
@@ -99,11 +123,11 @@ func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, ru
if quicTLSErr != nil {
log.Warn().Err(quicTLSErr).Msg("Failed to build QUIC probe TLS config")
quicCh <- tlsConfigErrResults(ProbeTypeQUIC, componentUDPConnectivity,
regionTargets, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, quicTLSErr), actionQUICBlocked)
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, quicTLSErr), actionQUICBlocked)
return
}
quicCh <- probeAllRegions(ctx, ProbeTypeQUIC, componentUDPConnectivity,
perRegionAddrs, regionTargets,
quicCh <- probeAllTargets(ctx, ProbeTypeQUIC, componentUDPConnectivity,
filteredAddrs, targetLabels,
func(addr *allregions.EdgeAddr) CheckResult {
return probeQUIC(ctx, quicTLSConfig, runDialers.QUICDialer, addr, log)
})
@@ -113,11 +137,11 @@ func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, ru
if http2TLSErr != nil {
log.Warn().Err(http2TLSErr).Msg("Failed to build HTTP/2 probe TLS config")
http2Ch <- tlsConfigErrResults(ProbeTypeHTTP2, componentTCPConnectivity,
regionTargets, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, http2TLSErr), actionHTTP2Blocked)
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, http2TLSErr), actionHTTP2Blocked)
return
}
http2Ch <- probeAllRegions(ctx, ProbeTypeHTTP2, componentTCPConnectivity,
perRegionAddrs, regionTargets,
http2Ch <- probeAllTargets(ctx, ProbeTypeHTTP2, componentTCPConnectivity,
filteredAddrs, targetLabels,
func(addr *allregions.EdgeAddr) CheckResult {
return probeHTTP2(ctx, http2TLSConfig, runDialers.TCPDialer, addr)
})
@@ -132,15 +156,15 @@ func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, ru
return Report{
RunID: runID,
Results: append(dnsResults, results.Collect()...),
SuggestedProtocol: suggestProtocol(results.QUIC, results.HTTP2),
SuggestedProtocol: suggestProtocol(results.QUIC, results.HTTP2, cfg.ProtocolOverride),
}
}
// tlsConfigErrResults returns one Fail CheckResult per region target, used when
// tlsConfigErrResults returns one Fail CheckResult per target, used when
// TLS config construction fails before any dial is attempted.
func tlsConfigErrResults(probeType ProbeType, component string, regionTargets []string, details, action string) []CheckResult {
results := make([]CheckResult, len(regionTargets))
for i, target := range regionTargets {
func tlsConfigErrResults(probeType ProbeType, component string, targets []string, details, action string) []CheckResult {
results := make([]CheckResult, len(targets))
for i, target := range targets {
results[i] = CheckResult{
Type: probeType,
Component: component,
@@ -153,47 +177,32 @@ func tlsConfigErrResults(probeType ProbeType, component string, regionTargets []
return results
}
func runDNSProbe(ctx context.Context, resolver DNSResolver, region string) ([][]*allregions.EdgeAddr, []CheckResult) {
var addrGroups [][]*allregions.EdgeAddr
var dnsResults []CheckResult
withRetry(ctx, maxRetries, func() bool {
addrGroups, dnsResults = probeDNS(resolver, region)
for _, r := range dnsResults {
if r.ProbeStatus == Fail {
return false
}
}
return len(dnsResults) > 0
})
return addrGroups, dnsResults
}
// probeAllRegions probes each region sequentially and returns one CheckResult
// per region. Within each region, all available addresses (V4 and/or V6) are
// tried and the best result is kept.
func probeAllRegions(
// probeAllTargets probes each target group sequentially and returns one
// CheckResult per group. Within each group, all available addresses (V4 and/or
// V6) are tried and the best result is kept.
func probeAllTargets(
ctx context.Context,
probeType ProbeType,
component string,
perRegionAddrs [][]*allregions.EdgeAddr,
regionTargets []string,
perGroupAddrs [][]*allregions.EdgeAddr,
targets []string,
probeFn func(*allregions.EdgeAddr) CheckResult,
) []CheckResult {
results := make([]CheckResult, len(perRegionAddrs))
for i, addrs := range perRegionAddrs {
results[i] = probeRegion(ctx, probeType, component, regionTargets[i], addrs, probeFn)
results := make([]CheckResult, len(perGroupAddrs))
for i, addrs := range perGroupAddrs {
results[i] = probeTarget(ctx, probeType, component, targets[i], addrs, probeFn)
}
return results
}
// probeRegion probes all addresses for a single region (typically one V4 and/or
// one V6) and returns the best result. Any address passing means the region is
// reachable, so Pass beats Fail within a region.
func probeRegion(
// probeTarget probes all addresses for a single target group (typically one V4
// and/or one V6) and returns the best result. Any address passing means the
// target is reachable, so Pass beats Fail within a group.
func probeTarget(
ctx context.Context,
probeType ProbeType,
component string,
regionTarget string,
target string,
addrs []*allregions.EdgeAddr,
probeFn func(*allregions.EdgeAddr) CheckResult,
) CheckResult {
@@ -201,7 +210,7 @@ func probeRegion(
return CheckResult{
Type: probeType,
Component: component,
Target: regionTarget,
Target: target,
ProbeStatus: Skip,
Details: "No suitable address found for configured IP version",
}
@@ -213,7 +222,7 @@ func probeRegion(
best = r
}
}
best.Target = regionTarget
best.Target = target
return best
}
@@ -238,11 +247,11 @@ func probeWithRetry(ctx context.Context, addr *allregions.EdgeAddr, probeFn func
return r
}
// addrsByRegion returns the addresses to probe for each DNS-resolved region,
// preserving the per-region grouping. Each inner slice contains at most one V4
// addrsByGroup returns the addresses to probe for each resolved target group,
// preserving the per-group structure. Each inner slice contains at most one V4
// and one V6 address (subject to ipVersion).
func addrsByRegion(addrGroups [][]*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) [][]*allregions.EdgeAddr {
perRegion := make([][]*allregions.EdgeAddr, 0, len(addrGroups))
func addrsByGroup(addrGroups [][]*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) [][]*allregions.EdgeAddr {
perGroup := make([][]*allregions.EdgeAddr, 0, len(addrGroups))
for _, group := range addrGroups {
v4, v6 := addrsByFamily(group, ipVersion)
var addrs []*allregions.EdgeAddr
@@ -252,27 +261,17 @@ func addrsByRegion(addrGroups [][]*allregions.EdgeAddr, ipVersion allregions.Con
if v6 != nil {
addrs = append(addrs, v6)
}
perRegion = append(perRegion, addrs)
perGroup = append(perGroup, addrs)
}
return perRegion
return perGroup
}
// dnsTargets extracts the Target hostname from each DNS CheckResult so that
// transport probe rows reuse the same region hostnames.
func dnsTargets(dnsResults []CheckResult) []string {
targets := make([]string, len(dnsResults))
for i, r := range dnsResults {
targets[i] = r.Target
}
return targets
}
// skipResultsForRegions returns one skip CheckResult per DNS region, using each
// region's hostname as the Target so the output table row aligns with its DNS row.
func skipResultsForRegions(dnsResults []CheckResult, probeType ProbeType, component string) []CheckResult {
results := make([]CheckResult, len(dnsResults))
for i, dns := range dnsResults {
results[i] = skipResult(probeType, component, dns.Target)
// skipResultsForTargets returns one skip CheckResult per entry in results,
// using each entry's Target label so the transport row aligns with its DNS row.
func skipResultsForTargets(targets []CheckResult, probeType ProbeType, component string) []CheckResult {
results := make([]CheckResult, len(targets))
for i, t := range targets {
results[i] = skipResult(probeType, component, t.Target, detailsDNSPrerequisiteFailed)
}
return results
}
@@ -304,10 +303,52 @@ func severity(s Status) int {
}
}
// suggestProtocol recommends QUIC when all QUIC region probes passed, HTTP/2
// when all HTTP/2 probes passed, and nil when neither transport works.
// parseProtocolOverride converts the raw --protocol flag string into a
// *connection.Protocol. It returns nil when the string is empty, "auto", or
// unrecognised, so the probe heuristic is used in those cases. "h2mux" is
// treated as HTTP/2 because both map to the same transport.
func parseProtocolOverride(flag string) *connection.Protocol {
switch flag {
case connection.QUIC.String():
p := connection.QUIC
return &p
case connection.HTTP2.String(), "h2mux":
p := connection.HTTP2
return &p
default:
// "auto", empty, or unknown — no override; let the heuristic decide.
return nil
}
}
// suggestProtocol determines the protocol to report in the pre-check summary.
//
// When the caller has explicitly overridden the protocol via --protocol, that
// choice is honoured when its transport probes produced evidence and did not
// fail.
//
// When there is no override (auto-selection), precedence is QUIC, HTTP/2,
// and nil. A protocol is only suggested if all probes pass.
//
// Any region failing means the transport is treated as failed (worst wins).
func suggestProtocol(quicResults, http2Results []CheckResult) *connection.Protocol {
func suggestProtocol(quicResults, http2Results []CheckResult, overrideFlag string) *connection.Protocol {
if override := parseProtocolOverride(overrideFlag); override != nil {
switch *override {
case connection.QUIC:
// Only report QUIC as the suggested protocol if its probes did not
// all fail — if they did, fall through to the heuristic so the
// summary can report a usable fallback or nil.
if len(quicResults) > 0 && worstStatus(quicResults) != Fail {
return new(connection.QUIC)
}
case connection.HTTP2:
// Same logic for an explicit HTTP/2 override.
if len(http2Results) > 0 && worstStatus(http2Results) != Fail {
return new(connection.HTTP2)
}
}
}
if len(quicResults) > 0 && worstStatus(quicResults) == Pass {
quic := connection.QUIC
return &quic
@@ -320,7 +361,7 @@ func suggestProtocol(quicResults, http2Results []CheckResult) *connection.Protoc
}
// withRetry calls fn up to 1+maxAttempts times, stopping as soon as fn returns
// true. Between attempts it sleeps with exponential backoff bounded by
// true. Between attempts, it sleeps with exponential backoff bounded by
// maxRetryDelay, and stops early if ctx is done.
func withRetry(ctx context.Context, maxAttempts int, fn func() bool) {
b := backoff.NewWithoutJitter(maxRetryDelay, retryBaseDelay)
+267 -22
View File
@@ -420,9 +420,174 @@ func TestRun_BothFamiliesProbed(t *testing.T) {
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
}
// TestRun_IPv4OnlySkipsV6 verifies that when IPv4Only is configured only V4
// addresses are probed (2 regions × 1 V4 = 2 dials per transport).
func TestRun_IPv4OnlySkipsV6(t *testing.T) {
// TestRun_IPVersionRestriction verifies that when a single IP family is
// configured, only that family is probed (2 regions × 1 addr = 2 dials per
// transport) and the excluded family is never dialled.
func TestRun_IPVersionRestriction(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ipVersion allregions.ConfigIPVersion
}{
{"IPv4Only skips V6", allregions.IPv4Only},
{"IPv6Only skips V4", allregions.IPv6Only},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
dns := mocks.NewMockDNSResolver(ctrl)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
// 2 regions × 1 addr per restricted family = 2 dials each.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&fakeQUICConn{}, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: tt.ipVersion},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
})
}
}
// TestRun_EdgeAddrs_SingleAddr verifies that a single --edge addr bypasses DNS
// probing. The report contains one DNS Skip row, transport rows labeled with
// the raw addr string, and the Management API row.
func TestRun_EdgeAddrs_SingleAddr(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
// DNS resolver must NOT be called when EdgeAddrs is set.
dns := mocks.NewMockDNSResolver(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Times(0)
// One addr resolves to one group → one dial per transport.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(1)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&fakeQUICConn{}, nil).Times(1)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
EdgeAddrs: []string{"127.0.0.1:7844"},
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 1 DNS Skip + 1 QUIC + 1 HTTP2 + 1 API = 4 results.
requireStatuses(t, report, Pass, Pass, Pass, Pass)
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip")
assert.Equal(t, "127.0.0.1:7844", report.Results[1].Target, "QUIC target must be the raw --edge addr")
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "HTTP2 target must be the raw --edge addr")
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
}
// TestRun_EdgeAddrs_MultipleAddrs verifies that multiple --edge addrs produce
// one transport row per addr, each labeled with its original addr string.
func TestRun_EdgeAddrs_MultipleAddrs(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns := mocks.NewMockDNSResolver(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Times(0)
// Two addrs → two groups → two dials per transport.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&fakeQUICConn{}, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
EdgeAddrs: []string{"127.0.0.1:7844", "127.0.0.2:7844"},
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass (one per addr) + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip addr1")
assert.Equal(t, "127.0.0.1:7844", report.Results[0].Target, "DNS skip addr1 label")
assert.Equal(t, ProbeTypeDNS, report.Results[1].Type, "second row must be DNS skip addr2")
assert.Equal(t, "127.0.0.2:7844", report.Results[1].Target, "DNS skip addr2 label")
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "QUIC addr1")
assert.Equal(t, "127.0.0.2:7844", report.Results[3].Target, "QUIC addr2")
assert.Equal(t, "127.0.0.1:7844", report.Results[4].Target, "HTTP2 addr1")
assert.Equal(t, "127.0.0.2:7844", report.Results[5].Target, "HTTP2 addr2")
}
// TestRun_EdgeAddrs_UnresolvableAddr verifies that when all --edge addrs fail
// to resolve, the DNS resolver is not called and transport rows are skipped,
// mirroring the DNS skip row.
func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns := mocks.NewMockDNSResolver(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Times(0)
// Unresolvable addr → no groups → no transport dials.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
EdgeAddrs: []string{"not-a-valid-addr"},
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 1 DNS Fail + 1 QUIC Skip + 1 HTTP2 Skip + 1 API = 4 results.
requireStatuses(t, report, Fail, Skip, Skip, Pass)
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type)
assert.Equal(t, "not-a-valid-addr", report.Results[0].Target)
assert.Equal(t, ProbeTypeQUIC, report.Results[1].Type)
assert.Equal(t, ProbeTypeHTTP2, report.Results[2].Type)
assert.Nil(t, report.SuggestedProtocol)
assert.True(t, report.hasHardFail())
}
// ---------------------------------------------------------------------------
// Protocol override tests
// ---------------------------------------------------------------------------
// TestRun_ProtocolOverride_HTTP2_BothPass verifies that when --protocol http2
// is set and both transports are reachable, the summary reports HTTP/2 (not
// QUIC, which would otherwise win the heuristic).
func TestRun_ProtocolOverride_HTTP2_BothPass(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
@@ -431,25 +596,33 @@ func TestRun_IPv4OnlySkipsV6(t *testing.T) {
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
// IPv4Only: only V4 addresses are probed → 2 regions × 1 V4 = 2 calls each.
// V6 addresses must never be dialed.
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
Return(nopConn{}, nil).AnyTimes()
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&fakeQUICConn{}, nil).Times(2)
Return(&fakeQUICConn{}, nil).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.IPv4Only},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
cfg := Config{
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
ProtocolOverride: "http2",
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
// Both transports pass, but the override must win — HTTP/2 is reported.
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol,
"override http2 should be reported even though QUIC probes also passed")
assert.False(t, report.hasHardFail())
}
// TestRun_IPv6OnlySkipsV4 verifies that when IPv6Only is configured only V6
// addresses are probed (2 regions × 1 V6 = 2 dials per transport).
func TestRun_IPv6OnlySkipsV4(t *testing.T) {
// TestRun_ProtocolOverride_QUIC_BothPass verifies that when --protocol quic is
// set and both transports are reachable, the summary reports QUIC (same as the
// heuristic would choose, but driven by the override).
func TestRun_ProtocolOverride_QUIC_BothPass(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
@@ -458,18 +631,90 @@ func TestRun_IPv6OnlySkipsV4(t *testing.T) {
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
// IPv6Only: only V6 addresses are probed → 2 regions × 1 V6 = 2 calls each.
// V4 addresses must never be dialled.
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
Return(nopConn{}, nil).AnyTimes()
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&fakeQUICConn{}, nil).Times(2)
Return(&fakeQUICConn{}, nil).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.IPv6Only},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
cfg := Config{
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
ProtocolOverride: "quic",
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
}
// TestRun_ProtocolOverride_HTTP2_QUICBlocked verifies that when --protocol http2
// is set and QUIC is blocked, we still report HTTP/2 (not a fallback to the
// heuristic, since the overridden transport is healthy).
func TestRun_ProtocolOverride_HTTP2_QUICBlocked(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
dns := mocks.NewMockDNSResolver(ctrl)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).AnyTimes()
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, errors.New("blocked")).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
ProtocolOverride: "http2",
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
}
// TestRun_ProtocolOverride_HTTP2_BothBlocked verifies that when --protocol http2
// is set but the HTTP/2 transport itself also fails (hard fail), the override
// falls through to the heuristic which returns nil — there is no usable protocol.
func TestRun_ProtocolOverride_HTTP2_BothBlocked(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
dns := mocks.NewMockDNSResolver(ctrl)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, errors.New("blocked")).AnyTimes()
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, errors.New("blocked")).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
ProtocolOverride: "http2",
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// The overridden transport (HTTP/2) is blocked, so the override cannot be
// honoured and the hard-fail path reports no suggested protocol.
assert.Nil(t, report.SuggestedProtocol)
assert.True(t, report.hasHardFail())
}
+80 -51
View File
@@ -17,6 +17,7 @@ import (
"github.com/cloudflare/cloudflared/connection"
edgedial "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tlsconfig"
)
@@ -25,7 +26,7 @@ const (
// Action messages for each probe outcome.
actionDNSFail = "Ensure your DNS resolver can resolve '%s'. Run: dig A %s @1.1.1.1. If that fails, contact your network administrator."
actionQUICBlocked = "QUIC traffic failed to connect to port 7844."
actionQUICBlocked = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
actionHTTP2Blocked = "Allow outbound TCP on port 7844."
actionAPIUnreachable = "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."
@@ -40,18 +41,19 @@ const (
targetPortQUIC = "Port 7844 (QUIC)"
targetPortHTTP2 = "Port 7844 (HTTP/2)"
targetAPI = "api.cloudflare.com:443"
noDNSTarget = "No DNS target (Using edge flag)"
// Details messages for CheckResult.
detailsNoAddressesReturned = "No addresses returned"
detailsResolvedSuccessfully = "Resolved successfully"
detailsHandshakeFailed = "Handshake failed"
detailsHandshakeSuccessful = "Handshake successful"
detailsBlockedOrUnreachable = "Blocked or unreachable"
detailsTLSHandshakeSuccessful = "TLS handshake successful"
detailsConnectionFailed = "Connection failed"
detailsTCPPortReachable = "TCP port reachable (TLS not validated)"
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
detailsTLSConfigFailed = "TLS configuration failed"
dnsNoAddressesReturned = "No addresses returned"
dnsResolvedSuccessfully = "DNS Resolved successfully"
detailsQUICHandshakeFailed = "QUIC connection failed"
detailsQUICHandshakeSuccessful = "QUIC connection successful"
detailsHTTP2BlockedOrUnreachable = "HTTP/2 connection is blocked or unreachable"
detailsHTTP2HandshakeSuccessful = "HTTP/2 connection successful"
detailsAPIConnectionFailed = "API Connection failed"
detailsApiReachable = "API is reachable"
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
detailsTLSConfigFailed = "TLS configuration failed"
// Region hostname templates.
region1Global = "region1.v2.argotunnel.com"
@@ -72,20 +74,6 @@ func (r *EdgeDNSResolver) Resolve(region string) ([][]*allregions.EdgeAddr, erro
return allregions.EdgeDiscovery(r.Log, allregions.RegionalServiceName(region))
}
// StaticEdgeDNSResolver implements DNSResolver for the --edge flag path.
type StaticEdgeDNSResolver struct {
Addrs []string
Log *zerolog.Logger
}
func (r *StaticEdgeDNSResolver) Resolve(_ string) ([][]*allregions.EdgeAddr, error) {
resolved := allregions.ResolveAddrs(r.Addrs, r.Log)
if len(resolved) == 0 {
return nil, fmt.Errorf("failed to resolve any edge address")
}
return [][]*allregions.EdgeAddr{resolved}, nil
}
type EdgeTCPDialer struct{}
func (d *EdgeTCPDialer) DialEdge(
@@ -109,7 +97,7 @@ func (d *EdgeQUICDialer) DialQuic(
connIndex uint8,
logger *zerolog.Logger,
opts dialopts.DialOpts,
) (quic.Connection, error) {
) (cfdquic.QUICConnection, error) {
return connection.DialQuic(ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
}
@@ -141,42 +129,47 @@ func probeTLSConfig(caCert string, p connection.Protocol) (*tls.Config, error) {
}
// probeDNS resolves edge addresses for the given region via the supplied
// DNSResolver and returns a CheckResult for each region discovered. If
// resolution fails for all regions, every result will carry StatusFail.
// DNSResolver and returns one ResolvedTarget per discovered region. If
// resolution fails entirely, every ResolvedTarget will carry a Fail DNSResult
// and nil Addrs.
func probeDNS(
resolver DNSResolver,
region string,
) ([][]*allregions.EdgeAddr, []CheckResult) {
) []ResolvedTarget {
region1Target, region2Target := regionTargets(region)
targets := []string{region1Target, region2Target}
addrGroups, err := resolver.Resolve(region)
if err != nil || len(addrGroups) == 0 {
detail := detailsNoAddressesReturned
detail := dnsNoAddressesReturned
if err != nil {
detail = err.Error()
}
return nil, []CheckResult{
newDNSCheckResult(region1Target, Fail, detail, fmt.Sprintf(actionDNSFail, region1Target, region1Target)),
newDNSCheckResult(region2Target, Fail, detail, fmt.Sprintf(actionDNSFail, region2Target, region2Target)),
return []ResolvedTarget{
{DNSResult: newDNSCheckResult(region1Target, Fail, detail, fmt.Sprintf(actionDNSFail, region1Target, region1Target))},
{DNSResult: newDNSCheckResult(region2Target, Fail, detail, fmt.Sprintf(actionDNSFail, region2Target, region2Target))},
}
}
targets := []string{region1Target, region2Target}
results := make([]CheckResult, 0, len(addrGroups))
for i, group := range addrGroups {
target := fmt.Sprintf("region%d.v2.argotunnel.com", i+1)
if i < len(targets) {
target = targets[i]
resolved := make([]ResolvedTarget, 0, len(addrGroups))
for i, target := range targets {
if i >= len(addrGroups) {
break
}
group := addrGroups[i]
if len(group) == 0 {
results = append(results, newDNSCheckResult(target, Fail, detailsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)))
resolved = append(resolved, ResolvedTarget{
DNSResult: newDNSCheckResult(target, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)),
})
} else {
results = append(results, newDNSCheckResult(target, Pass, detailsResolvedSuccessfully, ""))
resolved = append(resolved, ResolvedTarget{
Addrs: group,
DNSResult: newDNSCheckResult(target, Pass, dnsResolvedSuccessfully, ""),
})
}
}
return addrGroups, results
return resolved
}
// probeQUIC performs a QUIC handshake to a single edge address and returns a
@@ -217,7 +210,7 @@ func probeQUIC(
Component: componentUDPConnectivity,
Target: targetPortQUIC,
ProbeStatus: Fail,
Details: detailsHandshakeFailed,
Details: detailsQUICHandshakeFailed,
Action: actionQUICBlocked,
}
}
@@ -231,7 +224,7 @@ func probeQUIC(
Component: componentUDPConnectivity,
Target: targetPortQUIC,
ProbeStatus: Pass,
Details: detailsHandshakeSuccessful,
Details: detailsQUICHandshakeSuccessful,
}
}
@@ -251,7 +244,7 @@ func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, ad
Component: componentTCPConnectivity,
Target: targetPortHTTP2,
ProbeStatus: Fail,
Details: detailsBlockedOrUnreachable,
Details: detailsHTTP2BlockedOrUnreachable,
Action: actionHTTP2Blocked,
}
}
@@ -262,7 +255,7 @@ func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, ad
Component: componentTCPConnectivity,
Target: targetPortHTTP2,
ProbeStatus: Pass,
Details: detailsTLSHandshakeSuccessful,
Details: detailsHTTP2HandshakeSuccessful,
}
}
@@ -281,7 +274,7 @@ func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResul
Component: componentCloudflareAPI,
Target: targetAPI,
ProbeStatus: Fail,
Details: detailsConnectionFailed,
Details: detailsAPIConnectionFailed,
Action: actionAPIUnreachable,
}
}
@@ -292,17 +285,17 @@ func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResul
Component: componentCloudflareAPI,
Target: targetAPI,
ProbeStatus: Pass,
Details: detailsTCPPortReachable,
Details: detailsApiReachable,
}
}
func skipResult(probeType ProbeType, component, target string) CheckResult {
func skipResult(probeType ProbeType, component, target string, details string) CheckResult {
return CheckResult{
Type: probeType,
Component: component,
Target: target,
ProbeStatus: Skip,
Details: detailsDNSPrerequisiteFailed,
Details: details,
}
}
@@ -345,3 +338,39 @@ func addrsByFamily(group []*allregions.EdgeAddr, ipVersion allregions.ConfigIPVe
}
return
}
// runDNSProbe runs probeDNS with retry and returns []ResolvedTarget.
func runDNSProbe(ctx context.Context, resolver DNSResolver, region string) []ResolvedTarget {
var targets []ResolvedTarget
withRetry(ctx, maxRetries, func() bool {
targets = probeDNS(resolver, region)
for _, t := range targets {
if t.DNSResult.ProbeStatus == Fail {
return false
}
}
return len(targets) > 0
})
return targets
}
// resolveStaticEdge resolves each --edge addr individually, returning one
// ResolvedTarget per addr. Unresolvable addrs produce a Fail ResolvedTarget
// with nil Addrs so the report shows which addresses could not be reached.
func resolveStaticEdge(addrs []string, log *zerolog.Logger) []ResolvedTarget {
targets := make([]ResolvedTarget, 0, len(addrs))
for _, addr := range addrs {
resolved := allregions.ResolveAddrs([]string{addr}, log)
if len(resolved) > 0 {
targets = append(targets, ResolvedTarget{
Addrs: resolved,
DNSResult: newDNSCheckResult(addr, Pass, dnsResolvedSuccessfully, ""),
})
} else {
targets = append(targets, ResolvedTarget{
DNSResult: newDNSCheckResult(addr, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, addr, addr)),
})
}
}
return targets
}
+105 -49
View File
@@ -117,15 +117,14 @@ func TestProbeDNS_Success(t *testing.T) {
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr, v6Addr}}, nil)
addrs, results := probeDNS(resolver, "")
targets := probeDNS(resolver, "")
require.NotNil(t, addrs)
require.Len(t, results, 1)
assert.Len(t, addrs, 1)
assert.Equal(t, ProbeTypeDNS, results[0].Type)
assert.Equal(t, testRegion1Global, results[0].Target)
assert.Equal(t, Pass, results[0].ProbeStatus)
assert.Equal(t, detailsResolvedSuccessfully, results[0].Details)
require.Len(t, targets, 1)
assert.NotEmpty(t, targets[0].Addrs)
assert.Equal(t, ProbeTypeDNS, targets[0].DNSResult.Type)
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsResolvedSuccessfully, targets[0].DNSResult.Details)
}
func TestProbeDNS_MultipleRegions(t *testing.T) {
@@ -139,17 +138,17 @@ func TestProbeDNS_MultipleRegions(t *testing.T) {
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr1}, {v4Addr2}}, nil)
addrs, results := probeDNS(resolver, "")
targets := probeDNS(resolver, "")
require.NotNil(t, addrs)
require.Len(t, results, 2)
assert.Len(t, addrs, 2)
require.Len(t, targets, 2)
assert.Equal(t, testRegion1Global, results[0].Target)
assert.Equal(t, Pass, results[0].ProbeStatus)
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[0].Addrs)
assert.Equal(t, testRegion2Global, results[1].Target)
assert.Equal(t, Pass, results[1].ProbeStatus)
assert.Equal(t, testRegion2Global, targets[1].DNSResult.Target)
assert.Equal(t, Pass, targets[1].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[1].Addrs)
}
func TestProbeDNS_ResolverError(t *testing.T) {
@@ -160,17 +159,16 @@ func TestProbeDNS_ResolverError(t *testing.T) {
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return(nil, errors.New("DNS lookup failed"))
addrs, results := probeDNS(resolver, "")
targets := probeDNS(resolver, "")
assert.Nil(t, addrs)
require.Len(t, results, 2)
assert.Equal(t, Fail, results[0].ProbeStatus)
assert.Equal(t, "DNS lookup failed", results[0].Details)
assert.Contains(t, results[0].Action, testRegion1Global)
assert.Contains(t, results[1].Action, testRegion2Global)
assert.Equal(t, Fail, results[1].ProbeStatus)
require.Len(t, targets, 2)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, "DNS lookup failed", targets[0].DNSResult.Details)
assert.Contains(t, targets[0].DNSResult.Action, testRegion1Global)
assert.Empty(t, targets[1].Addrs)
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
assert.Contains(t, targets[1].DNSResult.Action, testRegion2Global)
}
func TestProbeDNS_EmptyResults(t *testing.T) {
@@ -181,12 +179,12 @@ func TestProbeDNS_EmptyResults(t *testing.T) {
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{}, nil)
addrs, results := probeDNS(resolver, "")
targets := probeDNS(resolver, "")
assert.Nil(t, addrs)
require.Len(t, results, 2)
assert.Equal(t, Fail, results[0].ProbeStatus)
assert.Equal(t, "No addresses returned", results[0].Details)
require.Len(t, targets, 2)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
}
func TestProbeDNS_EmptyGroup(t *testing.T) {
@@ -197,12 +195,12 @@ func TestProbeDNS_EmptyGroup(t *testing.T) {
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{}}, nil)
addrs, results := probeDNS(resolver, "")
targets := probeDNS(resolver, "")
require.NotNil(t, addrs)
require.Len(t, results, 1)
assert.Equal(t, Fail, results[0].ProbeStatus)
assert.Equal(t, "No addresses returned", results[0].Details)
require.Len(t, targets, 1)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
}
func TestProbeDNS_RegionFlag(t *testing.T) {
@@ -214,10 +212,10 @@ func TestProbeDNS_RegionFlag(t *testing.T) {
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("us").Return([][]*allregions.EdgeAddr{{v4Addr}}, nil)
_, results := probeDNS(resolver, "us")
targets := probeDNS(resolver, "us")
require.Len(t, results, 1)
assert.Equal(t, testRegion1US, results[0].Target)
require.Len(t, targets, 1)
assert.Equal(t, testRegion1US, targets[0].DNSResult.Target)
}
// probeQUIC tests.
@@ -238,7 +236,7 @@ func TestProbeQUIC_Success(t *testing.T) {
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
func TestProbeQUIC_DialError(t *testing.T) {
@@ -256,7 +254,7 @@ func TestProbeQUIC_DialError(t *testing.T) {
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsHandshakeFailed, result.Details)
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
assert.Equal(t, actionQUICBlocked, result.Action)
}
@@ -276,7 +274,7 @@ func TestProbeQUIC_CloseErrorDoesNotAffectResult(t *testing.T) {
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
func TestProbeQUIC_ContextTimeout(t *testing.T) {
@@ -293,7 +291,7 @@ func TestProbeQUIC_ContextTimeout(t *testing.T) {
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsHandshakeFailed, result.Details)
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
}
// probeHTTP2 tests.
@@ -312,7 +310,7 @@ func TestProbeHTTP2_Success(t *testing.T) {
assert.Equal(t, ProbeTypeHTTP2, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsTLSHandshakeSuccessful, result.Details)
assert.Equal(t, detailsHTTP2HandshakeSuccessful, result.Details)
}
func TestProbeHTTP2_DialError(t *testing.T) {
@@ -329,7 +327,7 @@ func TestProbeHTTP2_DialError(t *testing.T) {
assert.Equal(t, ProbeTypeHTTP2, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsBlockedOrUnreachable, result.Details)
assert.Equal(t, detailsHTTP2BlockedOrUnreachable, result.Details)
assert.Equal(t, actionHTTP2Blocked, result.Action)
}
@@ -349,7 +347,7 @@ func TestProbeManagementAPI_Success(t *testing.T) {
assert.Equal(t, "Cloudflare API", result.Component)
assert.Equal(t, "api.cloudflare.com:443", result.Target)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsTCPPortReachable, result.Details)
assert.Equal(t, detailsApiReachable, result.Details)
}
func TestProbeManagementAPI_DialError(t *testing.T) {
@@ -364,7 +362,7 @@ func TestProbeManagementAPI_DialError(t *testing.T) {
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsConnectionFailed, result.Details)
assert.Equal(t, detailsAPIConnectionFailed, result.Details)
assert.Equal(t, actionAPIUnreachable, result.Action)
}
@@ -373,7 +371,7 @@ func TestProbeManagementAPI_DialError(t *testing.T) {
func TestSkipResult(t *testing.T) {
t.Parallel()
result := skipResult(ProbeTypeQUIC, "UDP Connectivity", "Port 7844 (QUIC)")
result := skipResult(ProbeTypeQUIC, "UDP Connectivity", "Port 7844 (QUIC)", detailsDNSPrerequisiteFailed)
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, "UDP Connectivity", result.Component)
@@ -518,7 +516,7 @@ func TestProbeQUIC_IPv6Address(t *testing.T) {
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
// IPv6 address tests for probeHTTP2.
@@ -537,3 +535,61 @@ func TestProbeHTTP2_IPv6Address(t *testing.T) {
assert.Equal(t, Pass, result.ProbeStatus)
}
// resolveStaticEdge tests.
// TestResolveStaticEdge_SingleAddr verifies that a single resolvable --edge
// addr produces one group labeled with the original addr string.
func TestResolveStaticEdge_SingleAddr(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
targets := resolveStaticEdge([]string{"127.0.0.1:7844"}, &logger)
require.Len(t, targets, 1)
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[0].Addrs)
}
// TestResolveStaticEdge_MultipleAddrs verifies that multiple --edge addrs each
// produce their own ResolvedTarget, preserving per-addr structure and label order.
func TestResolveStaticEdge_MultipleAddrs(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "127.0.0.2:7844"}, &logger)
require.Len(t, targets, 2)
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
assert.Equal(t, "127.0.0.2:7844", targets[1].DNSResult.Target)
}
// TestResolveStaticEdge_InvalidAddr verifies that an unresolvable addr is
// silently skipped and does not appear in the output.
func TestResolveStaticEdge_InvalidAddr(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
// "not-a-valid-addr" has no port — ResolveTCPAddr will fail.
targets := resolveStaticEdge([]string{"not-a-valid-addr"}, &logger)
require.Len(t, targets, 1)
assert.Equal(t, "not-a-valid-addr", targets[0].DNSResult.Target)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
assert.Empty(t, targets[0].Addrs)
}
// TestResolveStaticEdge_PartiallyValid verifies that a mix of valid and invalid
// addrs produces one ResolvedTarget per addr — valid ones with Addrs and a Skip
// DNSResult, invalid ones with nil Addrs and a Fail DNSResult.
func TestResolveStaticEdge_PartiallyValid(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "not-a-valid-addr", "127.0.0.2:7844"}, &logger)
require.Len(t, targets, 3)
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[0].Addrs)
assert.Equal(t, "not-a-valid-addr", targets[1].DNSResult.Target)
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
assert.Empty(t, targets[1].Addrs)
assert.Equal(t, "127.0.0.2:7844", targets[2].DNSResult.Target)
assert.Equal(t, Pass, targets[2].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[2].Addrs)
}
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection/dialopts"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
)
@@ -44,7 +45,7 @@ type QUICDialer interface {
connIndex uint8,
logger *zerolog.Logger,
opts dialopts.DialOpts,
) (quic.Connection, error)
) (cfdquic.QUICConnection, error)
}
// ManagementDialer abstracts the TCP dial to api.cloudflare.com:443 used by
+14 -51
View File
@@ -10,19 +10,12 @@ import (
)
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"
@@ -35,8 +28,6 @@ const (
logFieldDetails = "details"
logFieldHardFail = "hard_fail"
logFieldSuggestedProtocol = "suggested_protocol"
sep = " "
)
// statusLabel returns the display label for a given Status.
@@ -58,21 +49,9 @@ 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 {
// automatically aligned columns, returning the rendered lines.
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)
@@ -81,27 +60,27 @@ func renderTable(results []CheckResult) string {
_, _ = 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()
return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n")
}
// 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 {
func renderActions(r Report) []string {
hardFail := r.hasHardFail()
var sb strings.Builder
actions := make([]string, 0)
for _, res := range r.Results {
if res.Action == "" || res.ProbeStatus != Fail {
continue
}
if hardFail {
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
actions = append(actions, fmt.Sprintf("ERROR: %s", res.Action))
} else {
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
actions = append(actions, fmt.Sprintf("WARNING: %s", res.Action))
}
}
return sb.String()
return actions
}
// summaryLine builds the SUMMARY: line based on the Report state.
@@ -181,28 +160,12 @@ func (r Report) hasWarn() bool {
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()
// String renders the Report as human-readable table lines suitable for logging.
func (r Report) String() []string {
lines := renderTable(r.Results)
lines = append(lines, renderActions(r)...)
lines = append(lines, "", summaryLine(r))
return lines
}
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
+85 -90
View File
@@ -25,11 +25,11 @@ func allPassReport() Report {
RunID: fixedRunID,
SuggestedProtocol: new(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"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: detailsQUICHandshakeSuccessful},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
},
}
}
@@ -41,18 +41,18 @@ func quicBlockedReport() Report {
RunID: fixedRunID,
SuggestedProtocol: new(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: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{
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.",
Details: detailsQUICHandshakeFailed,
Action: actionQUICBlocked,
},
{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"},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
},
}
}
@@ -64,10 +64,10 @@ func apiFailReport() Report {
RunID: fixedRunID,
SuggestedProtocol: new(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: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: detailsQUICHandshakeSuccessful},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
{
Type: ProbeTypeManagementAPI,
Component: "Cloudflare API",
@@ -86,14 +86,14 @@ func bothTransportsBlockedReport() Report {
RunID: fixedRunID,
SuggestedProtocol: nil,
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: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{
Type: ProbeTypeQUIC,
Component: "UDP Connectivity",
Target: "Port 7844 (QUIC)",
ProbeStatus: Fail,
Details: "Handshake failed",
Details: detailsQUICHandshakeFailed,
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
},
{
@@ -101,9 +101,9 @@ func bothTransportsBlockedReport() Report {
Component: "TCP Connectivity",
Target: "Port 7844 (HTTP/2)",
ProbeStatus: Fail,
Details: "Blocked or unreachable",
Details: detailsHTTP2BlockedOrUnreachable,
},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
},
}
}
@@ -134,85 +134,80 @@ func dnsFailReport() Report {
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"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"",
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.",
}
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"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.",
"",
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.",
}
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"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
"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.",
"",
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.",
}
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"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable",
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
"",
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
}
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"
want := []string{
"COMPONENT TARGET STATUS DETAILS",
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned",
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned",
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed",
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed",
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
"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.",
"",
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
}
assert.Equal(t, want, dnsFailReport().String())
}
@@ -221,9 +216,9 @@ func TestString_EmptyResults(t *testing.T) {
r := Report{RunID: fixedRunID, SuggestedProtocol: new(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())
require.Len(t, out, 3)
assert.Contains(t, out[0], "COMPONENT")
assert.Contains(t, out[2], "SUMMARY:")
}
// LogEvent() / structured log renderer tests
@@ -276,11 +271,11 @@ func TestLogEvent_AllPass(t *testing.T) {
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"},
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", detailsQUICHandshakeSuccessful},
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", detailsHTTP2HandshakeSuccessful},
{"Cloudflare API", "api.cloudflare.com:443", "pass", detailsApiReachable},
}
for i, exp := range expected {
e := entries[i]
@@ -312,7 +307,7 @@ func TestLogEvent_QuicBlocked(t *testing.T) {
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, "QUIC connection failed", quic.Details)
assert.Equal(t, fixedRunID.String(), quic.RunID)
// Summary: not a hard fail (HTTP/2 still works), protocol falls back to http2.
@@ -354,9 +349,9 @@ func TestLogEvent_BothTransportsBlocked(t *testing.T) {
// Both transport rows carry status=fail.
assert.Equal(t, "fail", entries[2].Status)
assert.Equal(t, "Handshake failed", entries[2].Details)
assert.Equal(t, "QUIC connection failed", entries[2].Details)
assert.Equal(t, "fail", entries[3].Status)
assert.Equal(t, "Blocked or unreachable", entries[3].Details)
assert.Equal(t, "HTTP/2 connection is blocked or unreachable", entries[3].Details)
summary := entries[len(entries)-1]
require.NotNil(t, summary.HardFail)
+28
View File
@@ -74,6 +74,19 @@ type CheckResult struct {
Action string
}
// ResolvedTarget bundles a resolved edge target's addresses with the DNS
// CheckResult that describes it. This keeps addr groups and their report rows
// together as a single unit, avoiding parallel-slice synchronization.
type ResolvedTarget struct {
// Addrs holds the resolved edge addresses for this target. May be empty
// when DNS resolution succeeded structurally but returned no IPs.
Addrs []*allregions.EdgeAddr
// DNSResult is the CheckResult representing DNS resolution for this target.
// Its Target field is the human-readable label used across all probe rows.
DNSResult CheckResult
}
// 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.
@@ -107,4 +120,19 @@ type Config struct {
// 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
// EdgeAddrs, when non-empty, contains the --edge flag values (explicit
// edge addresses). When set, DNS probing is skipped entirely — there are
// no SRV records to validate — and transport probes target each addr
// individually, labeled with the original addr string.
EdgeAddrs []string
// ProtocolOverride is the raw --protocol flag value (e.g. "quic",
// "http2", "h2mux"). When non-empty and not "auto", the pre-checks still
// probe both transports for diagnostic completeness, but the reported
// SuggestedProtocol honours the override so that the summary message
// reflects what cloudflared will actually use — not what the probe
// heuristic would recommend on its own. Parsing happens inside the
// prechecks package.
ProtocolOverride string
}
+8 -8
View File
@@ -137,7 +137,7 @@ func (p *Proxy) ProxyHTTP(
p.proxyLocalRequest(originProxy, w, req, isWebsocket)
return nil
default:
return fmt.Errorf("Unrecognized service: %s, %t", rule.Service, originProxy)
return fmt.Errorf("unrecognized service: %s, %t", rule.Service, originProxy)
}
}
@@ -193,7 +193,7 @@ func (p *Proxy) proxyHTTPRequest(
) error {
roundTripReq := tr.Request
if isWebsocket {
roundTripReq = tr.Clone(tr.Request.Context())
roundTripReq = tr.Clone(tr.Context())
roundTripReq.Header.Set("Connection", "Upgrade")
roundTripReq.Header.Set("Upgrade", "websocket")
roundTripReq.Header.Set("Sec-Websocket-Version", "13")
@@ -203,7 +203,7 @@ func (p *Proxy) proxyHTTPRequest(
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if disableChunkedEncoding {
roundTripReq.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(tr.Request.Header.Get("Content-Length"))
cLength, err := strconv.Atoi(tr.Header.Get("Content-Length"))
if err == nil {
roundTripReq.ContentLength = int64(cLength)
}
@@ -228,7 +228,7 @@ func (p *Proxy) proxyHTTPRequest(
}
tracing.EndWithStatusCode(ttfbSpan, resp.StatusCode)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
headers := make(http.Header, len(resp.Header))
// copy headers
@@ -249,11 +249,11 @@ func (p *Proxy) proxyHTTPRequest(
if !ok {
return errors.New("internal error: unsupported connection type")
}
defer rwc.Close()
defer func() { _ = rwc.Close() }()
eyeballStream := &bidirectionalStream{
writer: w,
reader: tr.Request.Body,
reader: tr.Body,
}
stream.Pipe(eyeballStream, rwc, logger)
@@ -292,7 +292,7 @@ func (p *Proxy) proxyStream(
return err
}
connectSpan.End()
defer originConn.Close()
defer func() { _ = originConn.Close() }()
logger.Debug().Msg("origin connection established")
encodedSpans := tr.GetSpans()
@@ -331,7 +331,7 @@ func (p *Proxy) proxyTCPStream(
return err
}
connectSpan.End()
defer originConn.Close()
defer func() { _ = originConn.Close() }()
logger.Debug().Msg("origin connection established")
encodedSpans := tr.GetSpans()
+10 -6
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/packet"
@@ -51,14 +50,14 @@ func (dm *DatagramMuxerV2) mtu() int {
}
type DatagramMuxerV2 struct {
session quic.Connection
session QUICConnection
logger *zerolog.Logger
sessionDemuxChan chan<- *packet.Session
packetDemuxChan chan Packet
}
func NewDatagramMuxerV2(
quicSession quic.Connection,
quicSession QUICConnection,
log *zerolog.Logger,
sessionDemuxChan chan<- *packet.Session,
) *DatagramMuxerV2 {
@@ -110,7 +109,8 @@ func (dm *DatagramMuxerV2) SendPacket(pk Packet) error {
return nil
}
// Demux reads datagrams from the QUIC connection and demuxes depending on whether it's a session or packet
// ServeReceive reads datagrams from the QUIC connection and demuxes them
// depending on whether it's a session or packet
func (dm *DatagramMuxerV2) ServeReceive(ctx context.Context) error {
for {
msg, err := dm.session.ReceiveDatagram(ctx)
@@ -144,8 +144,10 @@ func (dm *DatagramMuxerV2) demux(ctx context.Context, msgWithType []byte) error
switch msgType {
case DatagramTypeUDP:
return dm.handleSession(ctx, msg)
default:
case DatagramTypeIP, DatagramTypeIPWithTrace, DatagramTypeTracingSpan:
return dm.handlePacket(ctx, msg, msgType)
default:
return fmt.Errorf("unexpected datagram type %d", msgType)
}
}
@@ -189,8 +191,10 @@ func (dm *DatagramMuxerV2) handlePacket(ctx context.Context, pk []byte, msgType
Spans: spans,
TracingIdentity: tracingIdentity,
}
case DatagramTypeUDP:
return fmt.Errorf("unexpected datagram type %d in handlePacket", msgType)
default:
return fmt.Errorf("Unexpected datagram type %d", msgType)
return fmt.Errorf("unexpected datagram type %d", msgType)
}
select {
case <-ctx.Done():
+105
View File
@@ -0,0 +1,105 @@
package quic
import (
"context"
"errors"
"io"
"net"
"github.com/quic-go/quic-go"
)
// QUICConnection defines the subset of [quic.Connection] methods used by cloudflared.
// Consumers should accept this interface; producers should return [*ConnWithCloser].
type QUICConnection interface {
AcceptStream(ctx context.Context) (quic.Stream, error)
OpenStream() (quic.Stream, error)
OpenStreamSync(ctx context.Context) (quic.Stream, error)
CloseWithError(code quic.ApplicationErrorCode, reason string) error
Context() context.Context
SendDatagram(payload []byte) error
ReceiveDatagram(ctx context.Context) ([]byte, error)
LocalAddr() net.Addr
RemoteAddr() net.Addr
ConnectionState() quic.ConnectionState
}
// Compile-time assertion that *ConnWithCloser implements QUICConnection.
var _ QUICConnection = (*ConnWithCloser)(nil)
var (
// error returned when the [NewConnWithCloser] is called with a nil conn argument
ErrNilQuicConnection = errors.New("the provided quic connection is nil")
// error returned when the [NewConnWithCloser] is called with a nil closer argument
ErrNilCloser = errors.New("the provided closer is nil")
)
// ConnWithCloser wraps a [quic.Connection] and an [io.Closer] (typically the
// underlying [*net.UDPConn]). When [CloseWithError] is called the QUIC
// connection is closed first, then the closer is closed deterministically.
//
// A nil conn is only safe for [CloseWithError] (used in tests). All other
// delegated methods will panic on a nil conn.
type ConnWithCloser struct {
conn quic.Connection
closer io.Closer
}
// NewQUICConnection returns a [*ConnWithCloser] that will close closer after
// the QUIC connection is closed.
func NewQUICConnection(conn quic.Connection, closer io.Closer) (*ConnWithCloser, error) {
if conn == nil {
return nil, ErrNilQuicConnection
}
if closer == nil {
return nil, ErrNilCloser
}
return &ConnWithCloser{conn: conn, closer: closer}, nil
}
// CloseWithError closes the QUIC connection and then closes the underlying
// [io.Closer]. If both operations return errors, the errors are joined so that
// the closer error is no longer silently discarded.
func (c *ConnWithCloser) CloseWithError(code quic.ApplicationErrorCode, reason string) error {
connErr := c.conn.CloseWithError(code, reason)
closerErr := c.closer.Close()
return errors.Join(connErr, closerErr)
}
func (c *ConnWithCloser) AcceptStream(ctx context.Context) (quic.Stream, error) {
return c.conn.AcceptStream(ctx)
}
func (c *ConnWithCloser) OpenStream() (quic.Stream, error) {
return c.conn.OpenStream()
}
func (c *ConnWithCloser) OpenStreamSync(ctx context.Context) (quic.Stream, error) {
return c.conn.OpenStreamSync(ctx)
}
func (c *ConnWithCloser) Context() context.Context {
return c.conn.Context()
}
func (c *ConnWithCloser) SendDatagram(payload []byte) error {
return c.conn.SendDatagram(payload)
}
func (c *ConnWithCloser) ReceiveDatagram(ctx context.Context) ([]byte, error) {
return c.conn.ReceiveDatagram(ctx)
}
func (c *ConnWithCloser) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *ConnWithCloser) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *ConnWithCloser) ConnectionState() quic.ConnectionState {
return c.conn.ConnectionState()
}
+108
View File
@@ -0,0 +1,108 @@
package quic
import (
"errors"
"testing"
"github.com/quic-go/quic-go"
"github.com/stretchr/testify/require"
)
// mockCloser is an [io.Closer] that returns a configurable error.
type mockCloser struct {
closeErr error
}
func (m *mockCloser) Close() error {
return m.closeErr
}
// mockQuicConnection is a minimal test double for [quic.Connection].
type mockQuicConnection struct {
quic.Connection
closeWithErrorErr error
}
func (m *mockQuicConnection) CloseWithError(_ quic.ApplicationErrorCode, _ string) error {
return m.closeWithErrorErr
}
func TestNewConnWithCloser_NilConn(t *testing.T) {
t.Parallel()
conn, err := NewQUICConnection(nil, &mockCloser{})
require.ErrorIs(t, err, ErrNilQuicConnection)
require.Nil(t, conn)
}
func TestNewConnWithCloser_NilCloser(t *testing.T) {
t.Parallel()
conn, err := NewQUICConnection(&mockQuicConnection{}, nil)
require.ErrorIs(t, err, ErrNilCloser)
require.Nil(t, conn)
}
func TestNewConnWithCloser_Success(t *testing.T) {
t.Parallel()
qc := &mockQuicConnection{}
cl := &mockCloser{}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
require.NotNil(t, conn)
}
func TestConnWithCloser_CloseWithError_BothSucceed(t *testing.T) {
t.Parallel()
qc := &mockQuicConnection{}
cl := &mockCloser{}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.NoError(t, err)
}
func TestConnWithCloser_CloseWithError_QuicFails(t *testing.T) {
t.Parallel()
quicErr := errors.New("quic close failed")
qc := &mockQuicConnection{closeWithErrorErr: quicErr}
cl := &mockCloser{}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.ErrorIs(t, err, quicErr)
}
func TestConnWithCloser_CloseWithError_CloserFails(t *testing.T) {
t.Parallel()
closerErr := errors.New("closer failed")
qc := &mockQuicConnection{}
cl := &mockCloser{closeErr: closerErr}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.ErrorIs(t, err, closerErr)
}
func TestConnWithCloser_CloseWithError_BothFail(t *testing.T) {
t.Parallel()
quicErr := errors.New("quic close failed")
closerErr := errors.New("closer failed")
qc := &mockQuicConnection{closeWithErrorErr: quicErr}
cl := &mockCloser{closeErr: closerErr}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.ErrorIs(t, err, quicErr)
require.ErrorIs(t, err, closerErr)
}
// TestConnWithCloser_ImplementsInterface is a runtime assertion that
// *ConnWithCloser satisfies QUICConnection. The compile-time assertion is in
// quic_connection.go.
func TestConnWithCloser_ImplementsInterface(t *testing.T) {
t.Parallel()
var _ QUICConnection = (*ConnWithCloser)(nil)
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"schedule:nonOfficeHours"
],
"enabledManagers": [
"dockerfile"
],
"dockerfile": {
"managerFilePatterns": [
"/(^|/)Dockerfile$/",
"/(^|/)Dockerfile\\.amd64$/",
"/(^|/)Dockerfile\\.arm64$/"
]
},
"packageRules": [
{
"description": "Disable updates for everything by default; only the distroless base image is managed for now",
"matchPackageNames": [
"*"
],
"enabled": false
},
{
"description": "Keep any distroless base image up to date by pinning and updating its digest, since tags like :nonroot are rolling tags without a semver version",
"matchManagers": [
"dockerfile"
],
"enabled": true,
"pinDigests": true,
"matchPackageNames": [
"gcr.io/distroless/{/,}**"
]
}
]
}
+4 -5
View File
@@ -67,7 +67,6 @@ func (s *bidirectionalStreamStatus) wait(maxWaitForSecondStream time.Duration) e
// Only wait for second stream to finish if maxWait is greater than zero
if maxWaitForSecondStream > 0 {
timer := time.NewTimer(maxWaitForSecondStream)
defer timer.Stop()
@@ -87,14 +86,14 @@ func (s *bidirectionalStreamStatus) isAnyDone() bool {
// Pipe copies copy data to & from provided io.ReadWriters.
func Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
_ = PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
}
// PipeBidirectional copies data two BidirectionStreams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.
// PipeBidirectional copies data to two unidirectional streams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.
// The main difference is that when piping data from a reader to a writer, if EOF is read, then this implementation propagates the EOF signal to the destination/writer by closing the write side of the
// Bidirectional Stream.
// Finally, depending on once EOF is ready from one of the provided streams, the other direction of streaming data will have a configured time period to also finish, otherwise,
// the method will return immediately with a timeout error. It is however, the responsability of the caller to close the associated streams in both ends in order to free all the resources/go-routines.
// the method will return immediately with a timeout error. It is however, the responsibility of the caller to close the associated streams in both ends in order to free all the resources/go-routines.
func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error {
status := newBiStreamStatus()
@@ -129,7 +128,7 @@ func unidirectionalStream(dst WriterCloser, src Reader, dir string, status *bidi
}
}()
defer dst.CloseWrite()
defer func() { _ = dst.CloseWrite() }()
_, err := copyData(dst, src, dir)
if err != nil {
-3
View File
@@ -66,9 +66,6 @@ type TunnelConfig struct {
// NoPrechecks disables connectivity pre-checks at startup.
NoPrechecks bool
// Prechecks enables connectivity pre-checks at startup.
Prechecks bool
NamedTunnel *connection.TunnelProperties
ProtocolSelector connection.ProtocolSelector
EdgeTLSConfigs map[connection.Protocol]*tls.Config
+2 -2
View File
@@ -407,7 +407,7 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
func handleRedirects(req *http.Request, via []*http.Request, orgToken string) error {
// attach org token to login request
if strings.Contains(req.URL.Path, AccessLoginWorkerPath) {
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken})
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: orgToken}) //nolint: gosec
}
// attach app session cookie to authorized request
@@ -417,7 +417,7 @@ func handleRedirects(req *http.Request, via []*http.Request, orgToken string) er
if prevReq != nil && prevReq.Response != nil {
for _, c := range prevReq.Response.Cookies() {
if c.Name == appSessionCookie {
req.AddCookie(&http.Cookie{Name: appSessionCookie, Value: c.Value})
req.AddCookie(&http.Cookie{Name: appSessionCookie, Value: c.Value}) //nolint: gosec
return nil
}
}
+69 -2
View File
@@ -87,7 +87,7 @@ func main() {
// A good base middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.ClientIPFromRemoteAddr) // pick one ClientIPFrom* based on your infra, see below
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
@@ -221,6 +221,7 @@ type Router interface {
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Query(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
@@ -349,7 +350,11 @@ with `net/http` can be used with chi's mux.
| [Logger] | Logs the start and end of each request with the elapsed processing time |
| [NoCache] | Sets response headers to prevent clients from caching |
| [Profiler] | Easily attach net/http/pprof to your routers |
| [RealIP] | Sets a http.Request's RemoteAddr to either X-Real-IP or X-Forwarded-For |
| [ClientIPFromHeader] | Capture client IP from a trusted single-IP header (X-Real-IP, CF-Connecting-IP, ...) |
| [ClientIPFromXFF] | Capture client IP from X-Forwarded-For, skipping listed trusted CIDR prefixes |
| [ClientIPFromXFFTrustedProxies] | Capture client IP from X-Forwarded-For given a fixed number of trusted proxies |
| [ClientIPFromRemoteAddr] | Capture client IP from the TCP RemoteAddr (server directly on the public internet) |
| [RealIP] | Deprecated — vulnerable to IP spoofing; use [ClientIPFromXFF] or another ClientIPFrom\* middleware |
| [Recoverer] | Gracefully absorb panics and prints the stack trace |
| [RequestID] | Injects a request ID into the context of each request |
| [RedirectSlashes] | Redirect slashes on routing paths |
@@ -375,6 +380,12 @@ with `net/http` can be used with chi's mux.
[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger
[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache
[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler
[ClientIPFromHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromHeader
[ClientIPFromXFF]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFF
[ClientIPFromXFFTrustedProxies]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFFTrustedProxies
[ClientIPFromRemoteAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromRemoteAddr
[GetClientIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIP
[GetClientIPAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIPAddr
[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP
[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer
[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes
@@ -402,6 +413,62 @@ with `net/http` can be used with chi's mux.
[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts
[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter
### Choosing a ClientIP middleware
The legacy [RealIP] middleware is deprecated — it is vulnerable to IP spoofing
(GHSA-3fxj-6jh8-hvhx, GHSA-rjr7-jggh-pgcp, GHSA-9g5q-2w5x-hmxf) and mutates
`r.RemoteAddr`. Use one of the four `ClientIPFrom*` middlewares instead — pick
exactly one based on your network setup — and read the resulting IP with
[GetClientIP] (string) or [GetClientIPAddr] (`netip.Addr`):
| Your setup | Use |
|---|---|
| Directly on the public internet, no proxy | `middleware.ClientIPFromRemoteAddr` |
| Behind nginx (`X-Real-IP`), Cloudflare (`CF-Connecting-IP`), Apache (`X-Client-IP`) | `middleware.ClientIPFromHeader("<your-trusted-header>")` |
| Behind one or more proxies whose IP ranges you can list | `middleware.ClientIPFromXFF("10.0.0.0/8", ...)` |
| Behind a known, fixed number of proxies with dynamic IPs | `middleware.ClientIPFromXFFTrustedProxies(2)` |
```go
r := chi.NewRouter()
r.Use(middleware.RequestID)
// Pick exactly one. Examples for common deployments:
// Direct internet exposure (no proxy):
// r.Use(middleware.ClientIPFromRemoteAddr)
// Behind Cloudflare:
// r.Use(middleware.ClientIPFromHeader("CF-Connecting-IP"))
// Behind AWS CloudFront (or any proxy fleet with known CIDRs):
r.Use(middleware.ClientIPFromXFF(
"13.32.0.0/15", // CloudFront IPv4
"52.46.0.0/18", // CloudFront IPv4
"2600:9000::/28", // CloudFront IPv6
))
// Behind a known number of proxies with dynamic IPs:
// r.Use(middleware.ClientIPFromXFFTrustedProxies(2))
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
clientIP := middleware.GetClientIP(r.Context()) // for logs, rate-limit keys, etc.
_ = clientIP
})
```
These middlewares never mutate `r.RemoteAddr`. They store a normalized
`netip.Addr` in the request context — IPv4-mapped IPv6 (`::ffff:a.b.c.d`)
is folded to plain IPv4, and IPv6 zone identifiers carried in headers are
stripped, so one logical client maps to a single canonical key for logs,
rate limits, and ACLs.
See the per-function godoc for the full semantics of each middleware, and
[adam-p's "The perils of the 'real' client IP"](https://adam-p.ca/blog/2022/03/x-forwarded-for/)
for the underlying threat model.
### Extra middlewares & packages
Please see https://github.com/go-chi for additional packages.
+3 -2
View File
@@ -1,6 +1,6 @@
// Package chi is a small, idiomatic and composable router for building HTTP services.
//
// chi requires Go 1.14 or newer.
// chi supports the four most recent major versions of Go.
//
// Example:
//
@@ -77,7 +77,7 @@ type Router interface {
// path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) Router
// Route mounts a sub-Router along a `pattern`` string.
// Route mounts a sub-Router along a `pattern` string.
Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/*
@@ -102,6 +102,7 @@ type Router interface {
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Query(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
+5 -4
View File
@@ -133,11 +133,12 @@ func (x *Context) RoutePattern() string {
return routePattern
}
// replaceWildcards takes a route pattern and recursively replaces all
// occurrences of "/*/" to "/".
// replaceWildcards takes a route pattern and replaces all occurrences of
// "/*/" with "/". It iteratively runs until no wildcards remain to
// correctly handle consecutive wildcards.
func replaceWildcards(p string) string {
if strings.Contains(p, "/*/") {
return replaceWildcards(strings.Replace(p, "/*/", "/", -1))
for strings.Contains(p, "/*/") {
p = strings.ReplaceAll(p, "/*/", "/")
}
return p
}
+14 -9
View File
@@ -107,7 +107,8 @@ func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
// Handle adds the route `pattern` that matches any http method to
// execute the `handler` http.Handler.
func (mx *Mux) Handle(pattern string, handler http.Handler) {
if method, rest, found := strings.Cut(pattern, " "); found {
if i := strings.IndexAny(pattern, " \t"); i >= 0 {
method, rest := pattern[:i], strings.TrimLeft(pattern[i+1:], " \t")
mx.Method(method, rest, handler)
return
}
@@ -118,12 +119,7 @@ func (mx *Mux) Handle(pattern string, handler http.Handler) {
// HandleFunc adds the route `pattern` that matches any http method to
// execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
if method, rest, found := strings.Cut(pattern, " "); found {
mx.Method(method, rest, handlerFn)
return
}
mx.handle(mALL, pattern, handlerFn)
mx.Handle(pattern, handlerFn)
}
// Method adds the route `pattern` that matches `method` http method to
@@ -190,6 +186,12 @@ func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) {
mx.handle(mPUT, pattern, handlerFn)
}
// Query adds the route `pattern` that matches a QUERY http method to
// execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) Query(pattern string, handlerFn http.HandlerFunc) {
mx.handle(mQUERY, pattern, handlerFn)
}
// Trace adds the route `pattern` that matches a TRACE http method to
// execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) {
@@ -471,9 +473,12 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
// Find the route
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
if supportsPathValue {
setPathValue(rctx, r)
// Set http.Request path values from our request context
for i, key := range rctx.URLParams.Keys {
value := rctx.URLParams.Values[i]
r.SetPathValue(key, value)
}
r.Pattern = rctx.RoutePattern()
h.ServeHTTP(w, r)
return
-21
View File
@@ -1,21 +0,0 @@
//go:build go1.22 && !tinygo
// +build go1.22,!tinygo
package chi
import "net/http"
// supportsPathValue is true if the Go version is 1.22 and above.
//
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
const supportsPathValue = true
// setPathValue sets the path values in the Request value
// based on the provided request context.
func setPathValue(rctx *Context, r *http.Request) {
for i, key := range rctx.URLParams.Keys {
value := rctx.URLParams.Values[i]
r.SetPathValue(key, value)
}
}
-19
View File
@@ -1,19 +0,0 @@
//go:build !go1.22 || tinygo
// +build !go1.22 tinygo
package chi
import "net/http"
// supportsPathValue is true if the Go version is 1.22 and above.
//
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
const supportsPathValue = false
// setPathValue sets the path values in the Request value
// based on the provided request context.
//
// setPathValue is only supported in Go 1.22 and above so
// this is just a blank function so that it compiles.
func setPathValue(rctx *Context, r *http.Request) {
}
+27 -32
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"regexp"
"slices"
"sort"
"strconv"
"strings"
@@ -25,11 +26,17 @@ const (
mPATCH
mPOST
mPUT
mQUERY
mTRACE
)
var mALL = mCONNECT | mDELETE | mGET | mHEAD |
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
mOPTIONS | mPATCH | mPOST | mPUT | mQUERY | mTRACE
// methodQuery is the HTTP QUERY method (RFC 10008), a safe, idempotent
// method that conveys a request body. It is defined here until net/http
// provides an equivalent constant, at which point this is a 1-1 swap.
const methodQuery = "QUERY"
var methodMap = map[string]methodTyp{
http.MethodConnect: mCONNECT,
@@ -40,6 +47,7 @@ var methodMap = map[string]methodTyp{
http.MethodPatch: mPATCH,
http.MethodPost: mPOST,
http.MethodPut: mPUT,
methodQuery: mQUERY,
http.MethodTrace: mTRACE,
}
@@ -52,6 +60,7 @@ var reverseMethodMap = map[methodTyp]string{
mPATCH: http.MethodPatch,
mPOST: http.MethodPost,
mPUT: http.MethodPut,
mQUERY: methodQuery,
mTRACE: http.MethodTrace,
}
@@ -71,6 +80,7 @@ func RegisterMethod(method string) {
}
mt := methodTyp(2 << n)
methodMap[method] = mt
reverseMethodMap[mt] = method
mALL |= mt
}
@@ -328,7 +338,7 @@ func (n *node) replaceChild(label, tail byte, child *node) {
func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
nds := n.children[ntyp]
for i := 0; i < len(nds); i++ {
for i := range nds {
if nds[i].label == label && nds[i].tail == tail {
if ntyp == ntRegexp && nds[i].prefix != prefix {
continue
@@ -429,9 +439,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
}
// serially loop through each node grouped by the tail delimiter
for idx := 0; idx < len(nds); idx++ {
xn = nds[idx]
for _, xn = range nds {
// label for param nodes is the delimiter byte
p := strings.IndexByte(xsearch, xn.tail)
@@ -650,11 +658,9 @@ func (n *node) routes() []Route {
if h.handler == nil {
continue
}
m := methodTypString(mt)
if m == "" {
continue
if m, ok := reverseMethodMap[mt]; ok {
hs[m] = h.handler
}
hs[m] = h.handler
}
rt := Route{subroutes, hs, p}
@@ -772,29 +778,14 @@ func patParamKeys(pattern string) []string {
}
}
// longestPrefix finds the length of the shared prefix
// of two strings
func longestPrefix(k1, k2 string) int {
max := len(k1)
if l := len(k2); l < max {
max = l
}
var i int
for i = 0; i < max; i++ {
// longestPrefix finds the length of the shared prefix of two strings
func longestPrefix(k1, k2 string) (i int) {
for i = 0; i < min(len(k1), len(k2)); i++ {
if k1[i] != k2[i] {
break
}
}
return i
}
func methodTypString(method methodTyp) string {
for s, t := range methodMap {
if method == t {
return s
}
}
return ""
return
}
type nodes []*node
@@ -854,11 +845,15 @@ func Walk(r Routes, walkFn WalkFunc) error {
func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error {
for _, route := range r.Routes() {
mws := make([]func(http.Handler) http.Handler, len(parentMw))
copy(mws, parentMw)
mws = append(mws, r.Middlewares()...)
mws := slices.Concat(parentMw, r.Middlewares())
if route.SubRoutes != nil {
if handler, ok := route.Handlers["*"]; ok {
if chain, ok := handler.(*ChainHandler); ok {
mws = append(mws, chain.Middlewares...)
}
}
if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil {
return err
}
@@ -872,7 +867,7 @@ func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.H
}
fullRoute := parentRoute + route.Pattern
fullRoute = strings.Replace(fullRoute, "/*/", "/", -1)
fullRoute = strings.ReplaceAll(fullRoute, "/*/", "/")
if chain, ok := handler.(*ChainHandler); ok {
if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil {
+9 -1
View File
@@ -414,6 +414,9 @@ func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) {
// Decrypt the given payload and return the content encryption key.
func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) {
if recipient == nil {
return nil, errors.New("go-jose/go-jose: missing recipient")
}
epk, err := headers.getEPK()
if err != nil {
return nil, errors.New("go-jose/go-jose: invalid epk header")
@@ -461,13 +464,18 @@ func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientI
return nil, ErrUnsupportedAlgorithm
}
encryptedKey := recipient.encryptedKey
if len(encryptedKey) == 0 {
return nil, errors.New("go-jose/go-jose: missing JWE Encrypted Key")
}
key := deriveKey(string(algorithm), keySize)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return josecipher.KeyUnwrap(block, recipient.encryptedKey)
return josecipher.KeyUnwrap(block, encryptedKey)
}
func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) {
+9 -1
View File
@@ -66,12 +66,20 @@ func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) {
}
// KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher.
//
// https://datatracker.ietf.org/doc/html/rfc7518#section-4.4
// https://datatracker.ietf.org/doc/html/rfc7518#section-4.6
// https://datatracker.ietf.org/doc/html/rfc7518#section-4.8
func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) {
n := (len(ciphertext) / 8) - 1
if n <= 0 {
return nil, errors.New("go-jose/go-jose: JWE Encrypted Key too short")
}
if len(ciphertext)%8 != 0 {
return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks")
}
n := (len(ciphertext) / 8) - 1
r := make([][]byte, n)
for i := range r {
+18 -8
View File
@@ -366,11 +366,21 @@ func (ctx *symmetricKeyCipher) encryptKey(cek []byte, alg KeyAlgorithm) (recipie
// Decrypt the content encryption key.
func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) {
switch headers.getAlgorithm() {
case DIRECT:
cek := make([]byte, len(ctx.key))
copy(cek, ctx.key)
return cek, nil
if recipient == nil {
return nil, fmt.Errorf("go-jose/go-jose: missing recipient")
}
alg := headers.getAlgorithm()
if alg == DIRECT {
return bytes.Clone(ctx.key), nil
}
encryptedKey := recipient.encryptedKey
if len(encryptedKey) == 0 {
return nil, fmt.Errorf("go-jose/go-jose: missing JWE Encrypted Key")
}
switch alg {
case A128GCMKW, A192GCMKW, A256GCMKW:
aead := newAESGCM(len(ctx.key))
@@ -385,7 +395,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
parts := &aeadParts{
iv: iv.bytes(),
ciphertext: recipient.encryptedKey,
ciphertext: encryptedKey,
tag: tag.bytes(),
}
@@ -401,7 +411,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
return nil, err
}
cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey)
cek, err := josecipher.KeyUnwrap(block, encryptedKey)
if err != nil {
return nil, err
}
@@ -445,7 +455,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
return nil, err
}
cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey)
cek, err := josecipher.KeyUnwrap(block, encryptedKey)
if err != nil {
return nil, err
}
+1
View File
@@ -27,6 +27,7 @@ go_library(
"//internal/httprule",
"//utilities",
"@org_golang_google_genproto_googleapis_api//httpbody",
"@org_golang_google_grpc//:grpc",
"@org_golang_google_grpc//codes",
"@org_golang_google_grpc//grpclog",
"@org_golang_google_grpc//health/grpc_health_v1",
+3 -3
View File
@@ -201,13 +201,13 @@ func annotateContext(ctx context.Context, mux *ServeMux, req *http.Request, rpcM
if timeout != 0 {
ctx, _ = context.WithTimeout(ctx, timeout)
}
if len(pairs) == 0 {
return ctx, nil, nil
}
md := metadata.Pairs(pairs...)
for _, mda := range mux.metadataAnnotators {
md = metadata.Join(md, mda(ctx, req))
}
if len(md) == 0 {
return ctx, nil, nil
}
return ctx, md, nil
}
+3 -1
View File
@@ -28,7 +28,9 @@ func ForwardResponseStream(ctx context.Context, mux *ServeMux, marshaler Marshal
}
handleForwardResponseServerMetadata(w, mux, md)
w.Header().Set("Transfer-Encoding", "chunked")
if !mux.disableChunkedEncoding {
w.Header().Set("Transfer-Encoding", "chunked")
}
if err := handleForwardResponseOptions(ctx, w, nil, opts); err != nil {
HTTPError(ctx, mux, marshaler, w, req, err)
return
@@ -66,7 +66,7 @@ func (j *JSONPb) marshalTo(w io.Writer, v interface{}) error {
var (
// protoMessageType is stored to prevent constant lookup of the same type at runtime.
protoMessageType = reflect.TypeOf((*proto.Message)(nil)).Elem()
protoMessageType = reflect.TypeFor[proto.Message]()
)
// marshalNonProto marshals a non-message field of a protobuf message.
@@ -325,9 +325,9 @@ type protoEnum interface {
EnumDescriptor() ([]byte, []int)
}
var typeProtoEnum = reflect.TypeOf((*protoEnum)(nil)).Elem()
var typeProtoEnum = reflect.TypeFor[protoEnum]()
var typeProtoMessage = reflect.TypeOf((*proto.Message)(nil)).Elem()
var typeProtoMessage = reflect.TypeFor[proto.Message]()
// Delimiter for newline encoded JSON streams.
func (j *JSONPb) Delimiter() []byte {
+24 -5
View File
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/health/grpc_health_v1"
@@ -72,6 +73,7 @@ type ServeMux struct {
disablePathLengthFallback bool
unescapingMode UnescapingMode
writeContentLength bool
disableChunkedEncoding bool
}
// ServeMuxOption is an option that can be given to a ServeMux on construction.
@@ -124,6 +126,16 @@ func WithMiddlewares(middlewares ...Middleware) ServeMuxOption {
}
}
// WithDisableChunkedEncoding disables the Transfer-Encoding: chunked header
// for streaming responses. This is useful for streaming implementations that use
// Content-Length, which is mutually exclusive with Transfer-Encoding:chunked.
// Note that this option will not automatically add Content-Length headers, so it should be used with caution.
func WithDisableChunkedEncoding() ServeMuxOption {
return func(mux *ServeMux) {
mux.disableChunkedEncoding = true
}
}
// SetQueryParameterParser sets the query parameter parser, used to populate message from query parameters.
// Configuring this will mean the generated OpenAPI output is no longer correct, and it should be
// done with careful consideration.
@@ -281,15 +293,22 @@ func WithHealthEndpointAt(healthCheckClient grpc_health_v1.HealthClient, endpoin
http.MethodGet, endpointPath, func(w http.ResponseWriter, r *http.Request, _ map[string]string,
) {
_, outboundMarshaler := MarshalerForRequest(s, r)
resp, err := healthCheckClient.Check(r.Context(), &grpc_health_v1.HealthCheckRequest{
Service: r.URL.Query().Get("service"),
})
annotatedContext, err := AnnotateContext(r.Context(), s, r, grpc_health_v1.Health_Check_FullMethodName, WithHTTPPathPattern(endpointPath))
if err != nil {
s.errorHandler(r.Context(), s, outboundMarshaler, w, r, err)
return
}
var md ServerMetadata
resp, err := healthCheckClient.Check(annotatedContext, &grpc_health_v1.HealthCheckRequest{
Service: r.URL.Query().Get("service"),
}, grpc.Header(&md.HeaderMD), grpc.Trailer(&md.TrailerMD))
annotatedContext = NewServerMetadataContext(annotatedContext, md)
if err != nil {
s.errorHandler(annotatedContext, s, outboundMarshaler, w, r, err)
return
}
w.Header().Set("Content-Type", "application/json")
if resp.GetStatus() != grpc_health_v1.HealthCheckResponse_SERVING {
@@ -300,7 +319,7 @@ func WithHealthEndpointAt(healthCheckClient grpc_health_v1.HealthClient, endpoin
err = status.Error(codes.NotFound, resp.String())
}
s.errorHandler(r.Context(), s, outboundMarshaler, w, r, err)
s.errorHandler(annotatedContext, s, outboundMarshaler, w, r, err)
return
}
+12
View File
@@ -17,6 +17,7 @@ linters:
- ineffassign
- misspell
- modernize
- noctx
- perfsprint
- revive
- staticcheck
@@ -88,6 +89,16 @@ linters:
deny:
- pkg: go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal
desc: Do not use cross-module internal packages.
semconv:
list-mode: lax
files:
- "!**/semconv/**"
- "!**/exporters/zipkin/**"
deny:
- pkg: go.opentelemetry.io/otel/semconv
desc: "Use go.opentelemetry.io/otel/semconv/v1.40.0 instead. If a newer semconv version has been released, update the depguard rule."
allow:
- go.opentelemetry.io/otel/semconv/v1.40.0
gocritic:
disabled-checks:
- appendAssign
@@ -194,6 +205,7 @@ linters:
arguments:
- ["ID"] # AllowList
- ["Otel", "Aws", "Gcp"] # DenyList
- - skip-package-name-collision-with-go-std: true
- name: waitgroup-by-value
testifylint:
enable-all: true
+89 -1
View File
@@ -11,6 +11,90 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
<!-- Released section -->
<!-- Don't change this section unless doing release -->
## [1.43.0/0.65.0/0.19.0] 2026-04-02
### Added
- Add `IsRandom` and `WithRandom` on `TraceFlags`, and `IsRandom` on `SpanContext` in `go.opentelemetry.io/otel/trace` for [W3C Trace Context Level 2 Random Trace ID Flag](https://www.w3.org/TR/trace-context-2/#random-trace-id-flag) support. (#8012)
- Add service detection with `WithService` in `go.opentelemetry.io/otel/sdk/resource`. (#7642)
- Add `DefaultWithContext` and `EnvironmentWithContext` in `go.opentelemetry.io/otel/sdk/resource` to support plumbing `context.Context` through default and environment detectors. (#8051)
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8038)
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`. (#8038)
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#8038)
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#8038)
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8038)
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8038)
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8038)
- Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`.
Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060)
- Add `WithCardinalityLimitSelector` for metric reader for configuring cardinality limits specific to the instrument kind. (#7855)
### Changed
- Introduce the `EMPTY` Type in `go.opentelemetry.io/otel/attribute` to reflect that an empty value is now a valid value, with `INVALID` remaining as a deprecated alias of `EMPTY`. (#8038)
- Improve slice handling in `go.opentelemetry.io/otel/attribute` to optimize short slice values with fixed-size fast paths. (#8039)
- Improve performance of span metric recording in `go.opentelemetry.io/otel/sdk/trace` by returning early if self-observability is not enabled. (#8067)
- Improve formatting of metric data diffs in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8073)
### Deprecated
- Deprecate `INVALID` in `go.opentelemetry.io/otel/attribute`. Use `EMPTY` instead. (#8038)
### Fixed
- Return spec-compliant `TraceIdRatioBased` description. This is a breaking behavioral change, but it is necessary to
make the implementation [spec-compliant](https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased). (#8027)
- Fix a race condition in `go.opentelemetry.io/otel/sdk/metric` where the lastvalue aggregation could collect the value 0 even when no zero-value measurements were recorded. (#8056)
- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` to mitigate excessive memory usage caused by a misconfigured or malicious server.
Responses exceeding the limit are treated as non-retryable errors. (#8108)
- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` to mitigate excessive memory usage caused by a misconfigured or malicious server.
Responses exceeding the limit are treated as non-retryable errors. (#8108)
- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to mitigate excessive memory usage caused by a misconfigured or malicious server.
Responses exceeding the limit are treated as non-retryable errors. (#8108)
- `WithHostID` detector in `go.opentelemetry.io/otel/sdk/resource` to use full path for `kenv` command on BSD. (#8113)
- Fix missing `request.GetBody` in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to correctly handle HTTP2 GOAWAY frame. (#8096)
## [1.42.0/0.64.0/0.18.0/0.0.16] 2026-03-06
### Added
- Add `go.opentelemetry.io/otel/semconv/v1.40.0` package.
The package contains semantic conventions from the `v1.40.0` version of the OpenTelemetry Semantic Conventions.
See the [migration documentation](./semconv/v1.40.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.39.0`. (#7985)
- Add `Err` and `SetErr` on `Record` in `go.opentelemetry.io/otel/log` to attach an error and set record exception attributes in `go.opentelemetry.io/otel/log/sdk`. (#7924)
### Changed
- `TracerProvider.ForceFlush` in `go.opentelemetry.io/otel/sdk/trace` joins errors together and continues iteration through SpanProcessors as opposed to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856)
### Fixed
- Fix missing `request.GetBody` in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` to correctly handle HTTP2 GOAWAY frame. (#7931)
- Fix semconv v1.39.0 generated metric helpers skipping required attributes when extra attributes were empty. (#7964)
- Preserve W3C TraceFlags bitmask (including the random Trace ID flag) during trace context extraction and injection in `go.opentelemetry.io/otel/propagation`. (#7834)
### Removed
- Drop support for [Go 1.24]. (#7984)
## [1.41.0/0.63.0/0.17.0/0.0.15] 2026-03-02
This release is the last to support [Go 1.24].
The next release will require at least [Go 1.25].
### Added
- Support testing of [Go 1.26]. (#7902)
### Fixed
- Update `Baggage` in `go.opentelemetry.io/otel/propagation` and `Parse` and `New` in `go.opentelemetry.io/otel/baggage` to comply with W3C Baggage specification limits.
`New` and `Parse` now return partial baggage along with an error when limits are exceeded.
Errors from baggage extraction are reported to the global error handler. (#7880)
- Return an error when the endpoint is configured as insecure and with TLS configuration in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#7914)
- Return an error when the endpoint is configured as insecure and with TLS configuration in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#7914)
- Return an error when the endpoint is configured as insecure and with TLS configuration in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#7914)
## [1.40.0/0.62.0/0.16.0] 2026-02-02
### Added
@@ -3535,7 +3619,10 @@ It contains api and sdk for trace and meter.
- CircleCI build CI manifest files.
- CODEOWNERS file to track owners of this project.
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.40.0...HEAD
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.43.0...HEAD
[1.43.0/0.65.0/0.19.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.43.0
[1.42.0/0.64.0/0.18.0/0.0.16]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.42.0
[1.41.0/0.63.0/0.17.0/0.0.15]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.41.0
[1.40.0/0.62.0/0.16.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.40.0
[1.39.0/0.61.0/0.15.0/0.0.14]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.39.0
[1.38.0/0.60.0/0.14.0/0.0.13]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.38.0
@@ -3635,6 +3722,7 @@ It contains api and sdk for trace and meter.
<!-- Released section ended -->
[Go 1.26]: https://go.dev/doc/go1.26
[Go 1.25]: https://go.dev/doc/go1.25
[Go 1.24]: https://go.dev/doc/go1.24
[Go 1.23]: https://go.dev/doc/go1.23
+3 -3
View File
@@ -746,8 +746,8 @@ Encapsulate setup in constructor functions, ensuring clear ownership and scope:
import (
"errors"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.opentelemetry.io/otel/semconv/v1.39.0/otelconv"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
)
type SDKComponent struct {
@@ -1039,7 +1039,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
All observability metrics should follow the [OpenTelemetry Semantic Conventions for SDK metrics](https://github.com/open-telemetry/semantic-conventions/blob/1cf2476ae5e518225a766990a28a6d5602bd5a30/docs/otel/sdk-metrics.md).
Use the metric semantic conventions convenience package [otelconv](./semconv/v1.39.0/otelconv/metric.go).
Use the metric semantic conventions convenience package [otelconv](./semconv/v1.40.0/otelconv/metric.go).
##### Component Identification
+9 -6
View File
@@ -38,10 +38,14 @@ CROSSLINK = $(TOOLS)/crosslink
$(TOOLS)/crosslink: PACKAGE=go.opentelemetry.io/build-tools/crosslink
SEMCONVKIT = $(TOOLS)/semconvkit
SEMCONVKIT_FILES := $(sort $(shell find $(TOOLS_MOD_DIR)/semconvkit -type f))
$(TOOLS)/semconvkit: PACKAGE=go.opentelemetry.io/otel/$(TOOLS_MOD_DIR)/semconvkit
$(TOOLS)/semconvkit: $(SEMCONVKIT_FILES)
VERIFYREADMES = $(TOOLS)/verifyreadmes
VERIFYREADMES_FILES := $(sort $(shell find $(TOOLS_MOD_DIR)/verifyreadmes -type f))
$(TOOLS)/verifyreadmes: PACKAGE=go.opentelemetry.io/otel/$(TOOLS_MOD_DIR)/verifyreadmes
$(TOOLS)/verifyreadmes: $(VERIFYREADMES_FILES)
GOLANGCI_LINT = $(TOOLS)/golangci-lint
$(TOOLS)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/v2/cmd/golangci-lint
@@ -185,11 +189,10 @@ test-coverage: $(GOCOVMERGE)
.PHONY: benchmark
benchmark: $(OTEL_GO_MOD_DIRS:%=benchmark/%)
benchmark/%:
@echo "$(GO) test -run=xxxxxMatchNothingxxxxx -bench=. $*..." \
&& cd $* \
&& $(GO) list ./... \
| grep -v third_party \
| xargs $(GO) test -run=xxxxxMatchNothingxxxxx -bench=.
cd $* && $(GO) test -run='^$$' -bench=. $(ARGS) ./...
print-sharded-benchmarks:
@echo $(OTEL_GO_MOD_DIRS) | jq -cR 'split(" ")'
.PHONY: golangci-lint golangci-lint-fix
golangci-lint-fix: ARGS=--fix
@@ -215,7 +218,7 @@ go-mod-tidy/%: crosslink
&& $(GO) mod tidy -compat=1.21
.PHONY: lint
lint: misspell go-mod-tidy golangci-lint govulncheck
lint: misspell go-mod-tidy golangci-lint
.PHONY: vanity-import-check
vanity-import-check: $(PORTO)
+7 -7
View File
@@ -53,20 +53,20 @@ Currently, this project supports the following environments.
| OS | Go Version | Architecture |
|----------|------------|--------------|
| Ubuntu | 1.26 | amd64 |
| Ubuntu | 1.25 | amd64 |
| Ubuntu | 1.24 | amd64 |
| Ubuntu | 1.26 | 386 |
| Ubuntu | 1.25 | 386 |
| Ubuntu | 1.24 | 386 |
| Ubuntu | 1.26 | arm64 |
| Ubuntu | 1.25 | arm64 |
| Ubuntu | 1.24 | arm64 |
| macOS | 1.26 | amd64 |
| macOS | 1.25 | amd64 |
| macOS | 1.24 | amd64 |
| macOS | 1.26 | arm64 |
| macOS | 1.25 | arm64 |
| macOS | 1.24 | arm64 |
| Windows | 1.26 | amd64 |
| Windows | 1.25 | amd64 |
| Windows | 1.24 | amd64 |
| Windows | 1.26 | 386 |
| Windows | 1.25 | 386 |
| Windows | 1.24 | 386 |
While this project should work for other systems, no compatibility guarantees
are made for those systems currently.
+40 -1
View File
@@ -4,7 +4,9 @@
Create a `Version Release` issue to track the release process.
## Semantic Convention Generation
## Semantic Convention Upgrade
### Semantic Convention Generation
New versions of the [OpenTelemetry Semantic Conventions] mean new versions of the `semconv` package need to be generated.
The `semconv-generate` make target is used for this.
@@ -22,6 +24,43 @@ make semconv-generate # Uses the exported TAG.
This should create a new sub-package of [`semconv`](./semconv).
Ensure things look correct before submitting a pull request to include the addition.
The `CHANGELOG.md` should also be updated to reflect the new changes:
```md
- The `go.opentelemetry.io/otel/semconv/<NEW VERSION>` package. The package contains semantic conventions from the `<NEW VERSION>` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv/<NEW VERSION>/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/<PREVIOUS VERSION>`. (#PR_NUMBER)
```
> **Tip:** Change to the release and prior version to match the changes
### Update semconv imports
Once the new semconv module has been generated, update all semconv imports throughout the codebase to reference the new version:
```go
// Before
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/semconv/v1.37.0/otelconv"
// After
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.opentelemetry.io/otel/semconv/v1.39.0/otelconv"
```
Once complete, run `make` to check for any compilation or test failures.
#### Handling attribute changes
Some semconv releases might add new attributes or impact attributes that are currently being used. Changes could stem from a simple renaming, to more complex changes like merging attributes and property values being changed.
One should update the code to the new attributes that supersede the impacted ones, hence sticking to the semantic conventions. However, legacy attributes might still be emitted in accordance to the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable.
For an example on how such migration might have to be tracked and performed, see issue [#7806](https://github.com/open-telemetry/opentelemetry-go/issues/7806).
### Go contrib linter update
Update [.golangci.yml](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/.golangci.yml) in [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib/) to mandate the new semconv version.
## Breaking changes validation
You can run `make gorelease` which runs [gorelease](https://pkg.go.dev/golang.org/x/exp/cmd/gorelease) to ensure that there are no unwanted changes made in the public API.
+2 -2
View File
@@ -53,7 +53,7 @@ var (
_ Encoder = &defaultAttrEncoder{}
// encoderIDCounter is for generating IDs for other attribute encoders.
encoderIDCounter uint64
encoderIDCounter atomic.Uint64
defaultEncoderOnce sync.Once
defaultEncoderID = NewEncoderID()
@@ -64,7 +64,7 @@ var (
// once per each type of attribute encoder. Preferably in init() or in var
// definition.
func NewEncoderID() EncoderID {
return EncoderID{value: atomic.AddUint64(&encoderIDCounter, 1)}
return EncoderID{value: encoderIDCounter.Add(1)}
}
// DefaultEncoder returns an attribute encoder that encodes attributes in such
+3 -1
View File
@@ -27,6 +27,7 @@ const (
int64SliceID uint64 = 3762322556277578591 // "_[]int64" (little endian)
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
)
// hashKVs returns a new xxHash64 hash of kvs.
@@ -80,7 +81,8 @@ func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
for i := 0; i < rv.Len(); i++ {
h = h.String(rv.Index(i).String())
}
case INVALID:
case EMPTY:
h = h.Uint64(emptyID)
default:
// Logging is an alternative, but using the internal logger here
// causes an import cycle so it is not done.
+50 -67
View File
@@ -11,80 +11,63 @@ import (
"reflect"
)
// BoolSliceValue converts a bool slice into an array with same elements as slice.
func BoolSliceValue(v []bool) any {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[bool]())).Elem()
// sliceElem is the exact set of element types stored in attribute slice values.
// Using a closed set prevents accidental instantiations for unsupported types.
type sliceElem interface {
bool | int64 | float64 | string
}
// SliceValue converts a slice into an array with the same elements.
func SliceValue[T sliceElem](v []T) any {
// Keep only the common tiny-slice cases out of reflection. Extending this
// much further increases code size for diminishing benefit while larger
// slices still need the generic reflective path to preserve comparability.
// This matches the short lengths that show up most often in local
// benchmarks and semantic convention examples while leaving larger, less
// predictable slices on the generic reflective path.
switch len(v) {
case 0:
return [0]T{}
case 1:
return [1]T{v[0]}
case 2:
return [2]T{v[0], v[1]}
case 3:
return [3]T{v[0], v[1], v[2]}
}
return sliceValueReflect(v)
}
// AsSlice converts an array into a slice with the same elements.
func AsSlice[T sliceElem](v any) []T {
// Mirror the small fixed-array fast path used by SliceValue.
switch a := v.(type) {
case [0]T:
return []T{}
case [1]T:
return []T{a[0]}
case [2]T:
return []T{a[0], a[1]}
case [3]T:
return []T{a[0], a[1], a[2]}
}
return asSliceReflect[T](v)
}
func sliceValueReflect[T sliceElem](v []T) any {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[T]())).Elem()
reflect.Copy(cp, reflect.ValueOf(v))
return cp.Interface()
}
// Int64SliceValue converts an int64 slice into an array with same elements as slice.
func Int64SliceValue(v []int64) any {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]())).Elem()
reflect.Copy(cp, reflect.ValueOf(v))
return cp.Interface()
}
// Float64SliceValue converts a float64 slice into an array with same elements as slice.
func Float64SliceValue(v []float64) any {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[float64]())).Elem()
reflect.Copy(cp, reflect.ValueOf(v))
return cp.Interface()
}
// StringSliceValue converts a string slice into an array with same elements as slice.
func StringSliceValue(v []string) any {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[string]())).Elem()
reflect.Copy(cp, reflect.ValueOf(v))
return cp.Interface()
}
// AsBoolSlice converts a bool array into a slice into with same elements as array.
func AsBoolSlice(v any) []bool {
func asSliceReflect[T sliceElem](v any) []T {
rv := reflect.ValueOf(v)
if rv.Type().Kind() != reflect.Array {
if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[T]() {
return nil
}
cpy := make([]bool, rv.Len())
if len(cpy) > 0 {
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
}
return cpy
}
// AsInt64Slice converts an int64 array into a slice into with same elements as array.
func AsInt64Slice(v any) []int64 {
rv := reflect.ValueOf(v)
if rv.Type().Kind() != reflect.Array {
return nil
}
cpy := make([]int64, rv.Len())
if len(cpy) > 0 {
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
}
return cpy
}
// AsFloat64Slice converts a float64 array into a slice into with same elements as array.
func AsFloat64Slice(v any) []float64 {
rv := reflect.ValueOf(v)
if rv.Type().Kind() != reflect.Array {
return nil
}
cpy := make([]float64, rv.Len())
if len(cpy) > 0 {
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
}
return cpy
}
// AsStringSlice converts a string array into a slice into with same elements as array.
func AsStringSlice(v any) []string {
rv := reflect.ValueOf(v)
if rv.Type().Kind() != reflect.Array {
return nil
}
cpy := make([]string, rv.Len())
cpy := make([]T, rv.Len())
if len(cpy) > 0 {
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
}
+1 -1
View File
@@ -15,7 +15,7 @@ type KeyValue struct {
// Valid reports whether kv is a valid OpenTelemetry attribute.
func (kv KeyValue) Valid() bool {
return kv.Key.Defined() && kv.Value.Type() != INVALID
return kv.Key.Defined()
}
// Bool creates a KeyValue with a BOOL Value type.
+3 -3
View File
@@ -8,7 +8,7 @@ func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[INVALID-0]
_ = x[EMPTY-0]
_ = x[BOOL-1]
_ = x[INT64-2]
_ = x[FLOAT64-3]
@@ -19,9 +19,9 @@ func _() {
_ = x[STRINGSLICE-8]
}
const _Type_name = "INVALIDBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE"
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE"
var _Type_index = [...]uint8{0, 7, 11, 16, 23, 29, 38, 48, 60, 71}
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69}
func (i Type) String() string {
idx := int(i) - 0
+42 -19
View File
@@ -6,7 +6,6 @@ package attribute // import "go.opentelemetry.io/otel/attribute"
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
attribute "go.opentelemetry.io/otel/attribute/internal"
@@ -18,6 +17,8 @@ import (
type Type int // nolint: revive // redefines builtin Type.
// Value represents the value part in key-value pairs.
//
// Note that the zero value is a valid empty value.
type Value struct {
vtype Type
numeric uint64
@@ -26,8 +27,8 @@ type Value struct {
}
const (
// INVALID is used for a Value with no value set.
INVALID Type = iota
// EMPTY is used for a Value with no value set.
EMPTY Type = iota
// BOOL is a boolean Type Value.
BOOL
// INT64 is a 64-bit signed integral Type Value.
@@ -44,6 +45,10 @@ const (
FLOAT64SLICE
// STRINGSLICE is a slice of strings Type Value.
STRINGSLICE
// INVALID is used for a Value with no value set.
//
// Deprecated: Use EMPTY instead as an empty value is a valid value.
INVALID = EMPTY
)
// BoolValue creates a BOOL Value.
@@ -56,7 +61,7 @@ func BoolValue(v bool) Value {
// BoolSliceValue creates a BOOLSLICE Value.
func BoolSliceValue(v []bool) Value {
return Value{vtype: BOOLSLICE, slice: attribute.BoolSliceValue(v)}
return Value{vtype: BOOLSLICE, slice: attribute.SliceValue(v)}
}
// IntValue creates an INT64 Value.
@@ -64,16 +69,30 @@ func IntValue(v int) Value {
return Int64Value(int64(v))
}
// IntSliceValue creates an INTSLICE Value.
// IntSliceValue creates an INT64SLICE Value.
func IntSliceValue(v []int) Value {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]()))
for i, val := range v {
cp.Elem().Index(i).SetInt(int64(val))
}
return Value{
vtype: INT64SLICE,
slice: cp.Elem().Interface(),
val := Value{vtype: INT64SLICE}
// Avoid the common tiny-slice cases from allocating a new slice.
switch len(v) {
case 0:
val.slice = [0]int64{}
case 1:
val.slice = [1]int64{int64(v[0])}
case 2:
val.slice = [2]int64{int64(v[0]), int64(v[1])}
case 3:
val.slice = [3]int64{int64(v[0]), int64(v[1]), int64(v[2])}
default:
// Fallback to a new slice for larger slices.
cp := make([]int64, len(v))
for i, val := range v {
cp[i] = int64(val)
}
val.slice = attribute.SliceValue(cp)
}
return val
}
// Int64Value creates an INT64 Value.
@@ -86,7 +105,7 @@ func Int64Value(v int64) Value {
// Int64SliceValue creates an INT64SLICE Value.
func Int64SliceValue(v []int64) Value {
return Value{vtype: INT64SLICE, slice: attribute.Int64SliceValue(v)}
return Value{vtype: INT64SLICE, slice: attribute.SliceValue(v)}
}
// Float64Value creates a FLOAT64 Value.
@@ -99,7 +118,7 @@ func Float64Value(v float64) Value {
// Float64SliceValue creates a FLOAT64SLICE Value.
func Float64SliceValue(v []float64) Value {
return Value{vtype: FLOAT64SLICE, slice: attribute.Float64SliceValue(v)}
return Value{vtype: FLOAT64SLICE, slice: attribute.SliceValue(v)}
}
// StringValue creates a STRING Value.
@@ -112,7 +131,7 @@ func StringValue(v string) Value {
// StringSliceValue creates a STRINGSLICE Value.
func StringSliceValue(v []string) Value {
return Value{vtype: STRINGSLICE, slice: attribute.StringSliceValue(v)}
return Value{vtype: STRINGSLICE, slice: attribute.SliceValue(v)}
}
// Type returns a type of the Value.
@@ -136,7 +155,7 @@ func (v Value) AsBoolSlice() []bool {
}
func (v Value) asBoolSlice() []bool {
return attribute.AsBoolSlice(v.slice)
return attribute.AsSlice[bool](v.slice)
}
// AsInt64 returns the int64 value. Make sure that the Value's type is
@@ -155,7 +174,7 @@ func (v Value) AsInt64Slice() []int64 {
}
func (v Value) asInt64Slice() []int64 {
return attribute.AsInt64Slice(v.slice)
return attribute.AsSlice[int64](v.slice)
}
// AsFloat64 returns the float64 value. Make sure that the Value's
@@ -174,7 +193,7 @@ func (v Value) AsFloat64Slice() []float64 {
}
func (v Value) asFloat64Slice() []float64 {
return attribute.AsFloat64Slice(v.slice)
return attribute.AsSlice[float64](v.slice)
}
// AsString returns the string value. Make sure that the Value's type
@@ -193,7 +212,7 @@ func (v Value) AsStringSlice() []string {
}
func (v Value) asStringSlice() []string {
return attribute.AsStringSlice(v.slice)
return attribute.AsSlice[string](v.slice)
}
type unknownValueType struct{}
@@ -217,6 +236,8 @@ func (v Value) AsInterface() any {
return v.stringly
case STRINGSLICE:
return v.asStringSlice()
case EMPTY:
return nil
}
return unknownValueType{}
}
@@ -252,6 +273,8 @@ func (v Value) Emit() string {
return string(j)
case STRING:
return v.stringly
case EMPTY:
return ""
default:
return "unknown"
}
+83 -26
View File
@@ -14,8 +14,7 @@ import (
)
const (
maxMembers = 180
maxBytesPerMembers = 4096
maxMembers = 64
maxBytesPerBaggageString = 8192
listDelimiter = ","
@@ -29,7 +28,6 @@ var (
errInvalidProperty = errors.New("invalid baggage list-member property")
errInvalidMember = errors.New("invalid baggage list-member")
errMemberNumber = errors.New("too many list-members in baggage-string")
errMemberBytes = errors.New("list-member too large")
errBaggageBytes = errors.New("baggage-string too large")
)
@@ -309,10 +307,6 @@ func newInvalidMember() Member {
// an error if the input is invalid according to the W3C Baggage
// specification.
func parseMember(member string) (Member, error) {
if n := len(member); n > maxBytesPerMembers {
return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
}
var props properties
keyValue, properties, found := strings.Cut(member, propertyDelimiter)
if found {
@@ -430,6 +424,10 @@ type Baggage struct { //nolint:golint
// New returns a new valid Baggage. It returns an error if it results in a
// Baggage exceeding limits set in that specification.
//
// If the resulting Baggage exceeds the maximum allowed members or bytes,
// members are dropped until the limits are satisfied and an error is returned
// along with the partial result.
//
// It expects all the provided members to have already been validated.
func New(members ...Member) (Baggage, error) {
if len(members) == 0 {
@@ -441,7 +439,6 @@ func New(members ...Member) (Baggage, error) {
if !m.hasData {
return Baggage{}, errInvalidMember
}
// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
@@ -449,17 +446,42 @@ func New(members ...Member) (Baggage, error) {
}
}
// Check member numbers after deduplication.
var truncateErr error
// Check member count after deduplication.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
truncateErr = errors.Join(truncateErr, errMemberNumber)
for k := range b {
if len(b) <= maxMembers {
break
}
delete(b, k)
}
}
bag := Baggage{b}
if n := len(bag.String()); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
// Check byte size and drop members if necessary.
totalBytes := 0
first := true
for k := range b {
m := Member{
key: k,
value: b[k].Value,
properties: fromInternalProperties(b[k].Properties),
}
memberSize := len(m.String())
if !first {
memberSize++ // comma separator
}
if totalBytes+memberSize > maxBytesPerBaggageString {
truncateErr = errors.Join(truncateErr, fmt.Errorf("%w: %d", errBaggageBytes, totalBytes+memberSize))
delete(b, k)
continue
}
totalBytes += memberSize
first = false
}
return bag, nil
return Baggage{b}, truncateErr
}
// Parse attempts to decode a baggage-string from the passed string. It
@@ -470,36 +492,71 @@ func New(members ...Member) (Baggage, error) {
// defined (reading left-to-right) will be the only one kept. This diverges
// from the W3C Baggage specification which allows duplicate list-members, but
// conforms to the OpenTelemetry Baggage specification.
//
// If the baggage-string exceeds the maximum allowed members (64) or bytes
// (8192), members are dropped until the limits are satisfied and an error is
// returned along with the partial result.
//
// Invalid members are skipped and the error is returned along with the
// partial result containing the valid members.
func Parse(bStr string) (Baggage, error) {
if bStr == "" {
return Baggage{}, nil
}
if n := len(bStr); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
b := make(baggage.List)
sizes := make(map[string]int) // Track per-key byte sizes
var totalBytes int
var truncateErr error
for memberStr := range strings.SplitSeq(bStr, listDelimiter) {
// Check member count limit.
if len(b) >= maxMembers {
truncateErr = errors.Join(truncateErr, errMemberNumber)
break
}
m, err := parseMember(memberStr)
if err != nil {
return Baggage{}, err
truncateErr = errors.Join(truncateErr, err)
continue // skip invalid member, keep processing
}
// Check byte size limit.
// Account for comma separator between members.
memberBytes := len(m.String())
_, existingKey := b[m.key]
if !existingKey && len(b) > 0 {
memberBytes++ // comma separator only for new keys
}
// Calculate new totalBytes if we add/overwrite this key
var newTotalBytes int
if oldSize, exists := sizes[m.key]; exists {
// Overwriting existing key: subtract old size, add new size
newTotalBytes = totalBytes - oldSize + memberBytes
} else {
// New key
newTotalBytes = totalBytes + memberBytes
}
if newTotalBytes > maxBytesPerBaggageString {
truncateErr = errors.Join(truncateErr, errBaggageBytes)
break
}
// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
Properties: m.properties.asInternal(),
}
sizes[m.key] = memberBytes
totalBytes = newTotalBytes
}
// OpenTelemetry does not allow for duplicate list-members, but the W3C
// specification does. Now that we have deduplicated, ensure the baggage
// does not exceed list-member limits.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
if len(b) == 0 {
return Baggage{}, truncateErr
}
return Baggage{b}, nil
return Baggage{b}, truncateErr
}
// Member returns the baggage list-member identified by key.
+1 -1
View File
@@ -1,4 +1,4 @@
# This is a renovate-friendly source of Docker images.
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
FROM otel/weaver:v0.20.0@sha256:fa4f1c6954ecea78ab1a4e865bd6f5b4aaba80c1896f9f4a11e2c361d04e197e AS weaver
FROM otel/weaver:v0.22.1@sha256:33ae522ae4b71c1c562563c1d81f46aa0f79f088a0873199143a1f11ac30e5c9 AS weaver
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
+30
View File
@@ -199,3 +199,33 @@
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.
--------------------------------------------------------------------------------
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+1 -1
View File
@@ -94,7 +94,7 @@ func NewUnstarted(client Client) *Exporter {
}
// MarshalLog is the marshaling function used by the logging system to represent this Exporter.
func (e *Exporter) MarshalLog() interface{} {
func (e *Exporter) MarshalLog() any {
return struct {
Type string
Client Client
@@ -1,12 +1,15 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package tracetransform provides conversion functionality for the otlptrace
// exporters.
package tracetransform // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform"
import (
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/resource"
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
)
// KeyValues transforms a slice of attribute KeyValues into OTLP key-values.
@@ -90,6 +93,7 @@ func Value(v attribute.Value) *commonpb.AnyValue {
Values: stringSliceValues(v.AsStringSlice()),
},
}
case attribute.EMPTY:
default:
av.Value = &commonpb.AnyValue_StringValue{
StringValue: "INVALID",
@@ -4,8 +4,9 @@
package tracetransform // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform"
import (
"go.opentelemetry.io/otel/sdk/instrumentation"
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
"go.opentelemetry.io/otel/sdk/instrumentation"
)
func InstrumentationScope(il instrumentation.Scope) *commonpb.InstrumentationScope {
@@ -13,7 +14,8 @@ func InstrumentationScope(il instrumentation.Scope) *commonpb.InstrumentationSco
return nil
}
return &commonpb.InstrumentationScope{
Name: il.Name,
Version: il.Version,
Name: il.Name,
Version: il.Version,
Attributes: Iterator(il.Attributes.Iter()),
}
}
@@ -4,8 +4,9 @@
package tracetransform // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform"
import (
"go.opentelemetry.io/otel/sdk/resource"
resourcepb "go.opentelemetry.io/proto/otlp/resource/v1"
"go.opentelemetry.io/otel/sdk/resource"
)
// Resource transforms a Resource into an OTLP Resource.
@@ -4,12 +4,15 @@
package tracetransform // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform"
import (
"math"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/sdk/instrumentation"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
tracepb "go.opentelemetry.io/proto/otlp/trace/v1"
)
// Spans transforms a slice of OpenTelemetry spans into a slice of OTLP
@@ -95,26 +98,36 @@ func span(sd tracesdk.ReadOnlySpan) *tracepb.Span {
SpanId: sid[:],
TraceState: sd.SpanContext().TraceState().String(),
Status: status(sd.Status().Code, sd.Status().Description),
StartTimeUnixNano: uint64(sd.StartTime().UnixNano()),
EndTimeUnixNano: uint64(sd.EndTime().UnixNano()),
StartTimeUnixNano: uint64(max(0, sd.StartTime().UnixNano())), // nolint:gosec // Overflow checked.
EndTimeUnixNano: uint64(max(0, sd.EndTime().UnixNano())), // nolint:gosec // Overflow checked.
Links: links(sd.Links()),
Kind: spanKind(sd.SpanKind()),
Name: sd.Name(),
Attributes: KeyValues(sd.Attributes()),
Events: spanEvents(sd.Events()),
DroppedAttributesCount: uint32(sd.DroppedAttributes()),
DroppedEventsCount: uint32(sd.DroppedEvents()),
DroppedLinksCount: uint32(sd.DroppedLinks()),
DroppedAttributesCount: clampUint32(sd.DroppedAttributes()),
DroppedEventsCount: clampUint32(sd.DroppedEvents()),
DroppedLinksCount: clampUint32(sd.DroppedLinks()),
}
if psid := sd.Parent().SpanID(); psid.IsValid() {
s.ParentSpanId = psid[:]
}
s.Flags = buildSpanFlags(sd.Parent())
s.Flags = buildSpanFlagsWith(sd.SpanContext().TraceFlags(), sd.Parent())
return s
}
func clampUint32(v int) uint32 {
if v < 0 {
return 0
}
if int64(v) > math.MaxUint32 {
return math.MaxUint32
}
return uint32(v) // nolint: gosec // Overflow/Underflow checked.
}
// status transform a span code and message into an OTLP span status.
func status(status codes.Code, message string) *tracepb.Status {
var c tracepb.Status_StatusCode
@@ -142,31 +155,32 @@ func links(links []tracesdk.Link) []*tracepb.Span_Link {
for _, otLink := range links {
// This redefinition is necessary to prevent otLink.*ID[:] copies
// being reused -- in short we need a new otLink per iteration.
otLink := otLink
tid := otLink.SpanContext.TraceID()
sid := otLink.SpanContext.SpanID()
flags := buildSpanFlags(otLink.SpanContext)
flags := buildSpanFlagsWith(otLink.SpanContext.TraceFlags(), otLink.SpanContext)
sl = append(sl, &tracepb.Span_Link{
TraceId: tid[:],
SpanId: sid[:],
Attributes: KeyValues(otLink.Attributes),
DroppedAttributesCount: uint32(otLink.DroppedAttributeCount),
DroppedAttributesCount: clampUint32(otLink.DroppedAttributeCount),
Flags: flags,
})
}
return sl
}
func buildSpanFlags(sc trace.SpanContext) uint32 {
flags := tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
if sc.IsRemote() {
flags |= tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
func buildSpanFlagsWith(tf trace.TraceFlags, parent trace.SpanContext) uint32 {
// Lower 8 bits are the W3C TraceFlags; always indicate that we know whether the parent is remote
flags := uint32(tf) | uint32(tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK)
// Set the parent-is-remote bit when applicable
if parent.IsRemote() {
flags |= uint32(tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK)
}
return uint32(flags)
return flags // nolint:gosec // Flags is a bitmask and can't be negative
}
// spanEvents transforms span Events to an OTLP span events.
@@ -177,12 +191,12 @@ func spanEvents(es []tracesdk.Event) []*tracepb.Span_Event {
events := make([]*tracepb.Span_Event, len(es))
// Transform message events
for i := 0; i < len(es); i++ {
for i := range es {
events[i] = &tracepb.Span_Event{
Name: es[i].Name,
TimeUnixNano: uint64(es[i].Time.UnixNano()),
TimeUnixNano: uint64(max(0, es[i].Time.UnixNano())), // nolint:gosec // Overflow checked.
Attributes: KeyValues(es[i].Attributes),
DroppedAttributesCount: uint32(es[i].DroppedAttributeCount),
DroppedAttributesCount: clampUint32(es[i].DroppedAttributeCount),
}
}
return events
+1 -1
View File
@@ -5,5 +5,5 @@ package otlptrace // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
// Version is the current release version of the OpenTelemetry OTLP trace exporter in use.
func Version() string {
return "1.26.0"
return "1.43.0"
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package errorhandler provides the global error handler for OpenTelemetry.
//
// This package has no OTel dependencies, allowing it to be imported by any
// package in the module without creating import cycles.
package errorhandler // import "go.opentelemetry.io/otel/internal/errorhandler"
import (
"errors"
"log"
"sync"
"sync/atomic"
)
// ErrorHandler handles irremediable events.
type ErrorHandler interface {
// Handle handles any error deemed irremediable by an OpenTelemetry
// component.
Handle(error)
}
type ErrDelegator struct {
delegate atomic.Pointer[ErrorHandler]
}
// Compile-time check that delegator implements ErrorHandler.
var _ ErrorHandler = (*ErrDelegator)(nil)
func (d *ErrDelegator) Handle(err error) {
if eh := d.delegate.Load(); eh != nil {
(*eh).Handle(err)
return
}
log.Print(err)
}
// setDelegate sets the ErrorHandler delegate.
func (d *ErrDelegator) setDelegate(eh ErrorHandler) {
d.delegate.Store(&eh)
}
type errorHandlerHolder struct {
eh ErrorHandler
}
var (
globalErrorHandler = defaultErrorHandler()
delegateErrorHandlerOnce sync.Once
)
// GetErrorHandler returns the global ErrorHandler instance.
//
// The default ErrorHandler instance returned will log all errors to STDERR
// until an override ErrorHandler is set with SetErrorHandler. All
// ErrorHandler returned prior to this will automatically forward errors to
// the set instance instead of logging.
//
// Subsequent calls to SetErrorHandler after the first will not forward errors
// to the new ErrorHandler for prior returned instances.
func GetErrorHandler() ErrorHandler {
return globalErrorHandler.Load().(errorHandlerHolder).eh
}
// SetErrorHandler sets the global ErrorHandler to h.
//
// The first time this is called all ErrorHandler previously returned from
// GetErrorHandler will send errors to h instead of the default logging
// ErrorHandler. Subsequent calls will set the global ErrorHandler, but not
// delegate errors to h.
func SetErrorHandler(h ErrorHandler) {
current := GetErrorHandler()
if _, cOk := current.(*ErrDelegator); cOk {
if _, ehOk := h.(*ErrDelegator); ehOk && current == h {
// Do not assign to the delegate of the default ErrDelegator to be
// itself.
log.Print(errors.New("no ErrorHandler delegate configured"), " ErrorHandler remains its current value.")
return
}
}
delegateErrorHandlerOnce.Do(func() {
if def, ok := current.(*ErrDelegator); ok {
def.setDelegate(h)
}
})
globalErrorHandler.Store(errorHandlerHolder{eh: h})
}
func defaultErrorHandler() *atomic.Value {
v := &atomic.Value{}
v.Store(errorHandlerHolder{eh: &ErrDelegator{}})
return v
}
+7 -27
View File
@@ -5,33 +5,13 @@
package global // import "go.opentelemetry.io/otel/internal/global"
import (
"log"
"sync/atomic"
"go.opentelemetry.io/otel/internal/errorhandler"
)
// ErrorHandler handles irremediable events.
type ErrorHandler interface {
// Handle handles any error deemed irremediable by an OpenTelemetry
// component.
Handle(error)
}
// ErrorHandler is an alias for errorhandler.ErrorHandler, kept for backward
// compatibility with existing callers of internal/global.
type ErrorHandler = errorhandler.ErrorHandler
type ErrDelegator struct {
delegate atomic.Pointer[ErrorHandler]
}
// Compile-time check that delegator implements ErrorHandler.
var _ ErrorHandler = (*ErrDelegator)(nil)
func (d *ErrDelegator) Handle(err error) {
if eh := d.delegate.Load(); eh != nil {
(*eh).Handle(err)
return
}
log.Print(err)
}
// setDelegate sets the ErrorHandler delegate.
func (d *ErrDelegator) setDelegate(eh ErrorHandler) {
d.delegate.Store(&eh)
}
// ErrDelegator is an alias for errorhandler.ErrDelegator, kept for backward
// compatibility with existing callers of internal/global.
type ErrDelegator = errorhandler.ErrDelegator
+3 -33
View File
@@ -8,16 +8,13 @@ import (
"sync"
"sync/atomic"
"go.opentelemetry.io/otel/internal/errorhandler"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
type (
errorHandlerHolder struct {
eh ErrorHandler
}
tracerProviderHolder struct {
tp trace.TracerProvider
}
@@ -32,12 +29,10 @@ type (
)
var (
globalErrorHandler = defaultErrorHandler()
globalTracer = defaultTracerValue()
globalPropagators = defaultPropagatorsValue()
globalMeterProvider = defaultMeterProvider()
delegateErrorHandlerOnce sync.Once
delegateTraceOnce sync.Once
delegateTextMapPropagatorOnce sync.Once
delegateMeterOnce sync.Once
@@ -53,7 +48,7 @@ var (
// Subsequent calls to SetErrorHandler after the first will not forward errors
// to the new ErrorHandler for prior returned instances.
func GetErrorHandler() ErrorHandler {
return globalErrorHandler.Load().(errorHandlerHolder).eh
return errorhandler.GetErrorHandler()
}
// SetErrorHandler sets the global ErrorHandler to h.
@@ -63,26 +58,7 @@ func GetErrorHandler() ErrorHandler {
// ErrorHandler. Subsequent calls will set the global ErrorHandler, but not
// delegate errors to h.
func SetErrorHandler(h ErrorHandler) {
current := GetErrorHandler()
if _, cOk := current.(*ErrDelegator); cOk {
if _, ehOk := h.(*ErrDelegator); ehOk && current == h {
// Do not assign to the delegate of the default ErrDelegator to be
// itself.
Error(
errors.New("no ErrorHandler delegate configured"),
"ErrorHandler remains its current value.",
)
return
}
}
delegateErrorHandlerOnce.Do(func() {
if def, ok := current.(*ErrDelegator); ok {
def.setDelegate(h)
}
})
globalErrorHandler.Store(errorHandlerHolder{eh: h})
errorhandler.SetErrorHandler(h)
}
// TracerProvider is the internal implementation for global.TracerProvider.
@@ -174,12 +150,6 @@ func SetMeterProvider(mp metric.MeterProvider) {
globalMeterProvider.Store(meterProviderHolder{mp: mp})
}
func defaultErrorHandler() *atomic.Value {
v := &atomic.Value{}
v.Store(errorHandlerHolder{eh: &ErrDelegator{}})
return v
}
func defaultTracerValue() *atomic.Value {
v := &atomic.Value{}
v.Store(tracerProviderHolder{tp: &tracerProvider{}})
+3
View File
@@ -211,6 +211,9 @@ type Float64Observer interface {
//
// Use the WithAttributeSet (or, if performance is not a concern,
// the WithAttributes) option to include measurement attributes.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Observe(value float64, options ...ObserveOption)
}
+3
View File
@@ -210,6 +210,9 @@ type Int64Observer interface {
//
// Use the WithAttributeSet (or, if performance is not a concern,
// the WithAttributes) option to include measurement attributes.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Observe(value int64, options ...ObserveOption)
}
+53 -1
View File
@@ -30,6 +30,9 @@ type MeterProvider interface {
//
// If the name is empty, then an implementation defined default name will
// be used instead.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Meter(name string, opts ...MeterOption) Meter
}
@@ -51,6 +54,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Int64Counter(name string, options ...Int64CounterOption) (Int64Counter, error)
// Int64UpDownCounter returns a new Int64UpDownCounter instrument
@@ -61,6 +67,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Int64UpDownCounter(name string, options ...Int64UpDownCounterOption) (Int64UpDownCounter, error)
// Int64Histogram returns a new Int64Histogram instrument identified by
@@ -71,6 +80,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Int64Histogram(name string, options ...Int64HistogramOption) (Int64Histogram, error)
// Int64Gauge returns a new Int64Gauge instrument identified by name and
@@ -80,6 +92,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Int64Gauge(name string, options ...Int64GaugeOption) (Int64Gauge, error)
// Int64ObservableCounter returns a new Int64ObservableCounter identified
@@ -95,6 +110,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Int64ObservableCounter(name string, options ...Int64ObservableCounterOption) (Int64ObservableCounter, error)
// Int64ObservableUpDownCounter returns a new Int64ObservableUpDownCounter
@@ -110,6 +128,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Int64ObservableUpDownCounter(
name string,
options ...Int64ObservableUpDownCounterOption,
@@ -128,6 +149,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Int64ObservableGauge(name string, options ...Int64ObservableGaugeOption) (Int64ObservableGauge, error)
// Float64Counter returns a new Float64Counter instrument identified by
@@ -148,6 +172,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Float64UpDownCounter(name string, options ...Float64UpDownCounterOption) (Float64UpDownCounter, error)
// Float64Histogram returns a new Float64Histogram instrument identified by
@@ -158,6 +185,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Float64Histogram(name string, options ...Float64HistogramOption) (Float64Histogram, error)
// Float64Gauge returns a new Float64Gauge instrument identified by name and
@@ -167,6 +197,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Float64Gauge(name string, options ...Float64GaugeOption) (Float64Gauge, error)
// Float64ObservableCounter returns a new Float64ObservableCounter
@@ -182,6 +215,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Float64ObservableCounter(name string, options ...Float64ObservableCounterOption) (Float64ObservableCounter, error)
// Float64ObservableUpDownCounter returns a new
@@ -197,6 +233,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Float64ObservableUpDownCounter(
name string,
options ...Float64ObservableUpDownCounterOption,
@@ -215,6 +254,9 @@ type Meter interface {
// The name needs to conform to the OpenTelemetry instrument name syntax.
// See the Instrument Name section of the package documentation for more
// information.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
Float64ObservableGauge(name string, options ...Float64ObservableGaugeOption) (Float64ObservableGauge, error)
// RegisterCallback registers f to be called during the collection of a
@@ -229,6 +271,9 @@ type Meter interface {
// If no instruments are passed, f should not be registered nor called
// during collection.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
//
// The function f needs to be concurrent safe.
RegisterCallback(f Callback, instruments ...Observable) (Registration, error)
}
@@ -263,9 +308,15 @@ type Observer interface {
embedded.Observer
// ObserveFloat64 records the float64 value for obsrv.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
ObserveFloat64(obsrv Float64Observable, value float64, opts ...ObserveOption)
// ObserveInt64 records the int64 value for obsrv.
//
// Implementations of this method need to be safe for a user to call
// concurrently.
ObserveInt64(obsrv Int64Observable, value int64, opts ...ObserveOption)
}
@@ -283,6 +334,7 @@ type Registration interface {
// Unregister removes the callback registration from a Meter.
//
// This method needs to be idempotent and concurrent safe.
// Implementations of this method need to be idempotent and safe for a user
// to call concurrently.
Unregister() error
}

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