mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecb88678f1 | |||
| 43bfec0bcd | |||
| 86fccede6d | |||
| a53d9e5d44 | |||
| 9171757c66 | |||
| c06b2d43e9 | |||
| a9d023b7a2 | |||
| f76f75b449 | |||
| 5c66bd68ab | |||
| dba2d33a6b | |||
| 6b0571b598 | |||
| d5e0c2bb89 | |||
| 77e8965e28 | |||
| 8066821462 | |||
| 1e4ebe5471 | |||
| 02eb75b56d | |||
| 81a53555aa | |||
| 2bcaf09734 | |||
| 3315fa6e0f | |||
| ad11e67340 | |||
| 3a60f8ac0f | |||
| 68620efbce | |||
| 4d95ab73f5 | |||
| 57f7d693bb | |||
| ccffef1179 | |||
| 52519f67e8 | |||
| 0e84636de9 | |||
| 4177dd6936 | |||
| f6f60e1059 | |||
| 4494eee13d | |||
| 905d983d14 | |||
| 168f09cb4c | |||
| 0c9014870a | |||
| 31de04f858 | |||
| fbfd76089f |
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ include:
|
||||
rules:
|
||||
- !reference [.default-rules, run-always]
|
||||
tags:
|
||||
- windows-x86
|
||||
- canary-windows-x86
|
||||
cache: {}
|
||||
|
||||
##########################################
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
images:
|
||||
- name: cloudflared-daemon
|
||||
dockerfile: Dockerfile.$ARCH
|
||||
context: .
|
||||
version_file: versions-internal
|
||||
architectures:
|
||||
- amd64
|
||||
- arm64
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Generated
Vendored
+5
-1
@@ -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",
|
||||
|
||||
Generated
Vendored
+5
-3
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
Generated
Vendored
+2
-1
@@ -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.
|
||||
|
||||
Generated
Vendored
+32
-18
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user