Compare commits

...

38 Commits

Author SHA1 Message Date
João "Pisco" Fernandes 3a2b45c2a5 Release 2026.7.3 2026-07-22 17:32:46 +01:00
João "Pisco" Fernandes 78865b19bc fix: Bump golang.org/x/text and its dependencies to fix CVE 2026-07-22 16:49:49 +01:00
Rhys Rustad-Elliott 12e11208ae VULN-143514: Windows svc: use --token-file instead of --token
As with recent changes with Linux and MacOS, use --token-file instead of --token when installing the service for Windows.

The secret token was viewable by an unprivileged user by looking at the registry entry HKLM\\SYSTEM\\CurrentControlSet\\Services\\Cloudflared\\ImagePath, which stores the full command-line invocation of cloudflared (complete with --token ).

We fix this by storing the token in a file and restricting access to it.

The canonical way of protecting a secret token on Windows is to use the CryptProtectData and CryptUnprotectData functions in dpapi.h, which encrypt/unencrypt data using an OS-managed secret key. See here:

https://learn.microsoft.com/en-us/windows/win32/api/dpapi/

While we could use the DPAPI to encrypt/decrypt the token before writing it out to disk, this has two problems:

1) We would break existing Windows installs using --token-file with an unencrypted token file
2) We would introduce an inconsistency between how --token-file works on Linux/MacOS and Windows

Because of this, we keep things consistent and just add logic to cloudflared to protect the token file by modifying the permissions of the token file.

Windows's permission model differs completely from Linux and MacOS, so a Windows-specific function is used to restrict the token file's permissions. We strip ACLs from the file such that it's only readable by SYSTEM and Administrators.

Also done by this MR:

* Improve "service install --help" output on Windows to be in-line with Linux and MacOS
* Change uses of path.Join that work with file paths to be filepath.Join instead, which will use the correct platform-specific path separator (\\ on windows or / on \*nix) instead of only forward slashes
* Pull out constant string in MacOS service
2026-07-22 13:14:58 +00:00
Miguel da Costa Martins Marcelino 2206516c3b TUN-10701: Use curves for prechecks
Use curves when running pre-checks. Although it is not something critical, pre-checks should closely match the current cloudflared behavior when trying to establish connections to the edge. Adding curves here matches the current behavor.
2026-07-20 10:00:35 +00:00
João "Pisco" Fernandes 8679787525 Release 2026.7.2 2026-07-15 12:01:07 +01:00
Rhys Rustad-Elliott 45c0f22e97 VULN-118896: MacOS service: use --token-file instead of --token
* Refactor directory-related functions in macos_service.go to pull out common logic
* Use --token-file instead of --token when installing cloudflared with a token on MacOS such that the secret token will not show up in the output of "ps aux" (or similar)
* Improve the "service install" help text on MacOS to distinguish between the "service install " and "service install \<token\>" cases
2026-07-15 10:52:25 +01:00
João "Pisco" Fernandes e8956c10ac Update gcr.io/distroless/base-debian13:nonroot-arm Docker digest to 7a4876f88 2026-07-14 19:31:56 +01:00
Renovate 541a608dde Update gcr.io/distroless/base-debian13:nonroot-amd64 Docker digest to ce2a20e 2026-07-14 18:36:36 +01:00
Miguel da Costa Martins Marcelino 6fd720a4a4 chore: Change internal image name
Changing image name. The image is currently under `/tun/cloudflared/cloudflared-daemon/master`. We want to instead have it under `/tun/cloudflared/cloudflared/master`. Adding the `daemon` suffix does not add much value.
2026-07-14 16:43:53 +00:00
Renovate 60a8c3da17 Update gcr.io/distroless/base-debian13:nonroot Docker digest to b78832f
This MR contains the following updates:

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

---

### Configuration

📅 **Schedule**: (UTC)

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

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

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

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

---

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

---

This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTcuNSIsInVwZGF0ZWRJblZlciI6IjQzLjI1Ny41IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->
2026-07-14 17:40:32 +01:00
João "Pisco" Fernandes 2601f87b57 chore: Remove bookworm internal register publish
Bookworm has been deprecated and therefore our internal
repository no longer accepts the publish of new packages.
We are then removing the publish to the internal repositories of bookworm.
2026-07-13 11:28:14 +01:00
Rhys Rustad-Elliott f70adda11c VULN-118896: Linux service: Use --token-file instead of --token
When installed with a token, e.g.,

$ cloudflared service install \<token\>

cloudflared will set itself up to be run by the init system using the --token argument. This results in tokens being visible in the output of ps aux by unprivileged users. Change this such that this installation method instead puts the token in a file with mode 600 in /etc/cloudflared and uses the --token-file flag instead.
2026-07-13 09:57:07 +00:00
João "Pisco" Fernandes ecb88678f1 Release 2026.7.1 2026-07-09 13:06:21 +01:00
Miguel da Costa Martins Marcelino 43bfec0bcd Revert "TUN-10621: Propagate max wait timeout"
This reverts merge request !1859
2026-07-09 13:00:02 +01:00
João "Pisco" Fernandes 86fccede6d Release 2026.7.0 2026-07-08 13:08:05 +01:00
João "Pisco" Fernandes a53d9e5d44 Revert "TUN-10557: Bump quic-go v0.59.1"
This reverts commit 02eb75b56d.
2026-07-08 11:48:16 +01:00
João "Pisco" Fernandes 9171757c66 chore: Bump go-chi to version 5.3.1 2026-07-08 11:36:00 +01:00
Miguel da Costa Martins Marcelino c06b2d43e9 TUN-10621: Test websocket path
Adding a test that tries to exercise the race condition for the websocket path. The sequence is as follows:

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

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

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

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

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

---

### Configuration

📅 **Schedule**: (UTC)

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

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

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

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

---

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

---

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

Migrations:

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

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

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

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

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

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

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

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

Closes TUN-10563
2026-06-01 10:08:38 +01:00
146 changed files with 6451 additions and 6369 deletions
+8 -37
View File
@@ -3,23 +3,12 @@
runOnBranches: "^master$"
COMPONENT: "common"
.register_inputs_stable_bookworm: &register_inputs_stable_bookworm
<<: *register_inputs
runOnChangesTo: ['RELEASE_NOTES']
FLAVOR: "bookworm"
SERIES: "stable"
.register_inputs_stable_trixie: &register_inputs_stable_trixie
<<: *register_inputs
runOnChangesTo: ['RELEASE_NOTES']
FLAVOR: "trixie"
SERIES: "stable"
.register_inputs_next_bookworm: &register_inputs_next_bookworm
<<: *register_inputs
FLAVOR: "bookworm"
SERIES: next
.register_inputs_next_trixie: &register_inputs_next_trixie
<<: *register_inputs
FLAVOR: "trixie"
@@ -66,6 +55,10 @@ cloudflared-arm64-stable:
ORIGINAL_NAME: true
CGO_ENABLED: 1
# Jobs names
.amd64-stable: &amd64-stable ["cloudflared-amd64-stable"]
.arm64-stable: &arm64-stable ["cloudflared-arm64-stable"]
############
### Next ###
############
@@ -85,25 +78,16 @@ cloudflared-arm64-next:
<<: *arm64-stable-vars
NIGHTLY: true
# Jobs names
.amd64-next: &amd64-next ["cloudflared-amd64-next"]
.arm64-next: &arm64-next ["cloudflared-arm64-next"]
include:
- local: .ci/commons.gitlab-ci.yml
##########################################
### Publish Packages to Internal Repos ###
##########################################
# Bookworm AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_bookworm
jobPrefix: cloudflared-bookworm-amd64
needs: &amd64-stable ["cloudflared-amd64-stable"]
# Bookworm ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_bookworm
jobPrefix: cloudflared-bookworm-arm64
needs: &arm64-stable ["cloudflared-arm64-stable"]
# Trixie AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
@@ -122,19 +106,6 @@ include:
##################################################
### Publish Nightly Packages to Internal Repos ###
##################################################
# Bookworm AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_bookworm
jobPrefix: cloudflared-nightly-bookworm-amd64
needs: &amd64-next ['cloudflared-amd64-next']
# Bookworm ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_bookworm
jobPrefix: cloudflared-nightly-bookworm-arm64
needs: &arm64-next ['cloudflared-arm64-next']
# Trixie AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
+1 -1
View File
@@ -6,7 +6,7 @@ RUN apt-get update && \
apt-get install --no-install-recommends --allow-downgrades -y \
build-essential \
git \
go-boring=1.26.3-1 \
go-boring=1.26.4-1 \
libffi-dev \
procps \
python3-dev \
+34
View File
@@ -0,0 +1,34 @@
include:
- local: .ci/commons.gitlab-ci.yml
###########################################################################
### Build and Push Internal Image (commit SHA on master, version on tag) ###
###########################################################################
- component: $CI_SERVER_FQDN/cloudflare/ci/docker-image/build-push-image@~latest
inputs:
stage: release-internal
jobPrefix: internal-image
runOnMR: false
runOnBranches: '^master$'
needs:
- generate-internal-image-version
commentImageRefs: false
runner: vm-linux-x86-4cpu-8gb
EXTRA_DIB_ARGS: "--manifest=.docker-images-internal"
###############################################################################
### Generate Internal Image Version File ###
### Uses `git describe`: version tag on tagged commits, SHA-based on master ###
###############################################################################
generate-internal-image-version:
stage: release-internal
image: $BUILD_IMAGE
rules:
- !reference [.default-rules, run-on-master]
needs:
- ci-image-get-image-ref
script:
- make generate-internal-image-version
artifacts:
paths:
- versions-internal
+1 -1
View File
@@ -5,7 +5,7 @@
runner: linux-x86-8cpu-16gb
stage: build
golangVersion: "boring-1.26"
imageVersion: "3605-596a300@sha256:19fa512630b4c5681082c68fd98902e2f92092fc216412df44f7dda31cfa57c3"
imageVersion: "3625-1801d52@sha256:9261597bc2d229c997522848260de758567643d58ae1097196ae368db89a1d0f"
CGO_ENABLED: 1
.default-packaging-job: &packaging-job-defaults
+1 -1
View File
@@ -8,7 +8,7 @@ include:
rules:
- !reference [.default-rules, run-always]
tags:
- windows-x86
- canary-windows-x86
cache: {}
##########################################
+8
View File
@@ -0,0 +1,8 @@
images:
- name: cloudflared
dockerfile: Dockerfile.$ARCH
context: .
version_file: versions-internal
architectures:
- amd64
- arm64
+23 -17
View File
@@ -1,24 +1,30 @@
name: Semgrep OSS scan
on:
pull_request: {}
push:
branches: [main, master]
workflow_dispatch: {}
push:
branches:
- main
- master
schedule:
- cron: '0 0 * * *'
name: Semgrep config
- cron: '0 0 25 * *'
concurrency:
group: semgrep-${{ github.event_name }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-latest
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
SEMGREP_URL: https://cloudflare.semgrep.dev
SEMGREP_APP_URL: https://cloudflare.semgrep.dev
SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
container:
image: semgrep/semgrep
name: semgrep-oss
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@v4
- run: semgrep ci
- uses: actions/checkout@v5
with:
fetch-depth: 1
- id: cache-semgrep
uses: actions/cache@v5
with:
path: ~/.local
key: semgrep-1.160.0-${{ runner.os }}
- if: steps.cache-semgrep.outputs.cache-hit != 'true'
run: pip install --user semgrep==1.160.0
- run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- run: semgrep scan --config=auto
+6 -1
View File
@@ -1,5 +1,5 @@
variables:
GO_VERSION: "1.26.3"
GO_VERSION: "1.26.4"
MAC_GO_VERSION: "go@$GO_VERSION"
WIN_GO_VERSION: "go$GO_VERSION"
GIT_DEPTH: "0"
@@ -63,6 +63,11 @@ include:
#####################################################
- local: .ci/apt-internal.gitlab-ci.yml
#####################################################
########## Release Internal Docker Image ############
#####################################################
- local: .ci/internal-image.gitlab-ci.yml
#####################################################
############## Manual Claude Review #################
#####################################################
+2 -2
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.26.3 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:b78832f41c8128046807c24840ebee4f1c18ba7870eed423d8750c272c15e147
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+2 -2
View File
@@ -1,5 +1,5 @@
# use a builder image for building cloudflare
FROM golang:1.26.3 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:ce2a20e0e277b7d913aa8bcfa098fc2a543dc08028f7393434963fa24b39ea81
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+2 -2
View File
@@ -1,5 +1,5 @@
# use a builder image for building cloudflare
FROM golang:1.26.3 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:7a4876f88e7fe3190972c274b679b3473f61e8d990a4dce3627961b3e22a0eaf
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+4
View File
@@ -159,6 +159,10 @@ container:
generate-docker-version:
echo latest $(VERSION) > versions
.PHONY: generate-internal-image-version
generate-internal-image-version:
echo $(VERSION) > versions-internal
.PHONY: test
test: vet
+44
View File
@@ -1,3 +1,47 @@
2026.7.3
- 2026-07-22 VULN-143514: Windows svc: use --token-file instead of --token
- 2026-07-22 fix: Bump golang.org/x/text and its dependencies to fix CVE
- 2026-07-20 TUN-10701: Use curves for prechecks
2026.7.2
- 2026-07-15 VULN-118896: MacOS service: use --token-file instead of --token
- 2026-07-14 Update gcr.io/distroless/base-debian13:nonroot Docker digest to b78832f
- 2026-07-14 chore: Change internal image name
- 2026-07-14 Update gcr.io/distroless/base-debian13:nonroot-arm Docker digest to 7a4876f88
- 2026-07-13 VULN-118896: Linux service: Use --token-file instead of --token
- 2026-07-13 chore: Remove bookworm internal register publish
- 2026-07-13 Update gcr.io/distroless/base-debian13:nonroot-amd64 Docker digest to ce2a20e
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
+2 -3
View File
@@ -17,8 +17,7 @@ import (
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
// This implements the functions for glider proxy (sock5) and the carrier interface
type Websocket struct {
log *zerolog.Logger
isSocks bool
log *zerolog.Logger
}
// NewWSConnection returns a new connection object
@@ -36,7 +35,7 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
ws.log.Err(err).Str(LogFieldOriginURL, options.OriginURL).Msg("failed to connect to origin")
return err
}
defer wsConn.Close()
defer func() { _ = wsConn.Close() }()
stream.Pipe(wsConn, conn, ws.log)
return nil
+22 -16
View File
@@ -2,10 +2,11 @@ package carrier
import (
"context"
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"math/rand"
"math/big"
"testing"
"time"
@@ -23,7 +24,7 @@ import (
func websocketClientTLSConfig(t *testing.T) *tls.Config {
certPool := x509.NewCertPool()
helloCert, err := tlsconfig.GetHelloCertificateX509()
assert.NoError(t, err)
require.NoError(t, err)
certPool.AddCert(helloCert)
assert.NotNil(t, certPool)
return &tls.Config{RootCAs: certPool}
@@ -43,8 +44,8 @@ func TestServe(t *testing.T) {
shutdownC := make(chan struct{})
errC := make(chan error)
listener, err := hello.CreateTLSListener("localhost:1111")
assert.NoError(t, err)
defer listener.Close()
require.NoError(t, err)
defer func() { _ = listener.Close() }()
go func() {
errC <- hello.StartHelloWorldServer(&log, listener, shutdownC)
@@ -56,19 +57,21 @@ func TestServe(t *testing.T) {
assert.NotNil(t, tlsConfig)
d := gws.Dialer{TLSClientConfig: tlsConfig}
conn, resp, err := clientConnect(req, &d)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
for i := 0; i < 1000; i++ {
messageSize := rand.Int()%2048 + 1
clientMessage := make([]byte, messageSize)
// rand.Read always returns len(clientMessage) and a nil error
rand.Read(clientMessage)
messageSize, err := crand.Int(crand.Reader, big.NewInt(2048))
require.NoError(t, err)
clientMessage := make([]byte, int(messageSize.Int64())+1)
_, err = crand.Read(clientMessage)
require.NoError(t, err)
err = conn.WriteMessage(websocket.BinaryFrame, clientMessage)
assert.NoError(t, err)
require.NoError(t, err)
messageType, message, err := conn.ReadMessage()
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, websocket.BinaryFrame, messageType)
assert.Equal(t, clientMessage, message)
}
@@ -97,27 +100,30 @@ func TestWebsocketWrapper(t *testing.T) {
req := testRequest(t, testAddr, nil)
conn, resp, err := clientConnect(req, &d)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
// Websocket now connected to test server so lets check our wrapper
wrapper := cfwebsocket.GorillaConn{Conn: conn}
buf := make([]byte, 100)
wrapper.Write([]byte("abc"))
_, err = wrapper.Write([]byte("abc"))
require.NoError(t, err)
n, err := wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 3)
require.Equal(t, 3, n)
require.Equal(t, "abc", string(buf[:n]))
// Test partial read, read 1 of 3 bytes in one read and the other 2 in another read
wrapper.Write([]byte("abc"))
_, err = wrapper.Write([]byte("abc"))
require.NoError(t, err)
buf = buf[:1]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 1)
require.Equal(t, 1, n)
require.Equal(t, "a", string(buf[:n]))
buf = buf[:cap(buf)]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 2)
require.Equal(t, 2, n)
require.Equal(t, "bc", string(buf[:n]))
}
+81 -14
View File
@@ -1,6 +1,11 @@
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
@@ -8,23 +13,85 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
)
func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
token := c.Args().First()
const (
defaultTokenFile = "token"
)
func ensureConfigDirExists(configDir string) error {
if err := os.Mkdir(configDir, 0o755); err != nil { //nolint:gosec // config dir must be traversable by non-root user
if errors.Is(err, os.ErrExist) {
return nil
}
return fmt.Errorf("create config dir at %s: %w", configDir, err)
}
return nil
}
func createTokenFileUnix(path string) error {
const tokenPerms os.FileMode = 0o600
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, tokenPerms) //nolint:gosec // All callers of this function construct path from constant strings or well-known env vars (e.g., $HOME)
if err != nil {
return fmt.Errorf("create token file at %s: %w", path, err)
}
defer func() { _ = f.Close() }()
// If the file already existed with unrestrictive permissions, os.OpenFile
// will not update its permissions, so perform an extra os.Chmod
if err := os.Chmod(path, tokenPerms); err != nil {
return fmt.Errorf("chmod token file at %s: %w", path, err)
}
return nil
}
// Write out the token file to the configuration directory with the correct
// permissions. Since the method used to restrict the permissions is platform
// dependent, make the function used to restrict the permissions an injectable
// dependency
func writeTokenToFile(path string, token string) error {
if _, err := tunnel.ParseToken(token); err != nil {
return nil, cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
return cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
}
if err := createTokenFile(path); err != nil {
return fmt.Errorf("create token file at %s: %w", path, err)
}
// Won't update permissions as file already exists
if err := os.WriteFile(path, []byte(token), 0o600); err != nil {
return fmt.Errorf("write token to %s: %w", path, err)
}
return nil
}
func removeTokenFile(configDir string, log *zerolog.Logger) {
tp := tokenPath(configDir)
err := os.Remove(tp)
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Warn().Msgf("Could not remove service token file at %s: %v", tp, err)
}
}
func buildArgsForTokenFile(configDir string) []string {
return []string{
"tunnel", "run", "--token", token,
}, nil
}
func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) {
if c.NArg() > 0 {
// currently, we only support extra args for token
return buildArgsForToken(c, log)
} else {
// empty extra args
return make([]string, 0), nil
"tunnel", "run", "--token-file", tokenPath(configDir),
}
}
func tokenPath(configDir string) string {
return filepath.Join(configDir, defaultTokenFile)
}
func writeTokenToConfigDir(c *cli.Context, configDir string) error {
if err := ensureConfigDirExists(configDir); err != nil {
return err
}
if err := writeTokenToFile(tokenPath(configDir), c.Args().First()); err != nil {
return err
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
// Package inits provides detection of the init system managing the host
// (systemd, OpenRC, or SysV). It is shared by the service installer and the
// auto-updater so that init-system detection lives in a single place.
//
// The functions are safe to call on any GOOS; on non-Linux platforms they
// report that no Linux init system is in use.
package inits
import (
"os"
"runtime"
)
// IsSystemd reports whether the host is managed by systemd.
func IsSystemd() bool {
_, err := os.Stat("/run/systemd/system")
return err == nil
}
// IsOpenRC reports whether the host is managed by OpenRC.
func IsOpenRC() bool {
for _, path := range []string{"/sbin/openrc-run", "/usr/sbin/openrc-run", "/usr/bin/openrc-run"} {
if _, err := os.Stat(path); err == nil {
return true
}
}
return false
}
// IsSysV reports whether the host relies on a SysV-style init system, i.e. a
// Linux host that is managed by neither systemd nor OpenRC. systemd and OpenRC
// keep the service alive themselves, so only SysV needs the process to restart
// itself after an auto-update.
func IsSysV() bool {
if runtime.GOOS != "linux" {
return false
}
return !IsSystemd() && !IsOpenRC()
}
+153 -41
View File
@@ -3,6 +3,7 @@
package main
import (
"errors"
"fmt"
"io"
"os"
@@ -11,6 +12,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/inits"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
@@ -22,8 +24,21 @@ func runApp(app *cli.App, _ chan struct{}) {
Usage: "Manages the cloudflared system service",
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install cloudflared as a system service",
Name: "install",
Usage: "Install cloudflared as a system service",
ArgsUsage: "[TOKEN]",
Description: `
Installs cloudflared as a service using the detected init system (e.g., sysv,
systemd, openrc).
A token may optionally be provided. If a token is provided, it will be written
to disk in the service configuration directory and the cloudflared service
configured to use it via the --token-file argument.
If no token is provided, cloudflared will attempt to find a configuration file
with tunnel credentials from a predetermined list of configuration directory
paths. If found, it will use that configuration file and credentials (or error
out if no configuration file with credentials was found).`,
Action: cliutil.ConfiguredAction(installLinuxService),
Flags: []cli.Flag{
noUpdateServiceFlag,
@@ -49,13 +64,17 @@ const (
cloudflaredService = "cloudflared.service"
cloudflaredUpdateService = "cloudflared-update.service"
cloudflaredUpdateTimer = "cloudflared-update.timer"
cloudflaredOpenRCService = "cloudflared"
)
// OS-specific function for token file creation
var createTokenFile = createTokenFileUnix
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 +116,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 +129,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 +205,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)
@@ -210,27 +262,46 @@ func installLinuxService(c *cli.Context) error {
Path: etPath,
}
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
var extraArgs []string
if c.NArg() == 0 {
extraArgsFunc = buildArgsForConfig
// If passed no arguments e.g., "$ cloudflared service install",
// install the service using the detected config file (or error-out if
// no config exists).
if extraArgs, err = buildArgsForConfig(c, log); err != nil {
return err
}
} else {
extraArgsFunc = buildArgsForToken
}
// If passed one argument e.g., "$ cloudflared service install <token>"
// write the token to the config directory and install the service
// using --token-file pointing to that file. This is the quick setup
// the tunnel UI suggests.
extraArgs, err := extraArgsFunc(c, log)
if err != nil {
return err
// Ensure token file is removed if install fails
defer func() {
if err != nil {
removeTokenFile(serviceConfigDir, log)
}
}()
if err = writeTokenToConfigDir(c, serviceConfigDir); err != nil {
return fmt.Errorf("could not write token to configuration directory: %w", err)
}
extraArgs = buildArgsForTokenFile(serviceConfigDir)
}
templateArgs.ExtraArgs = extraArgs
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
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 +329,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,22 +416,51 @@ 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)
}
removeTokenFile(serviceConfigDir, log)
if err == nil {
log.Info().Msg("Linux service for cloudflared uninstalled successfully")
}
return err
}
@@ -371,7 +468,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 +532,39 @@ func uninstallSysv(log *zerolog.Logger) error {
return nil
}
func ensureConfigDirExists(configDir string) error {
ok, err := config.FileExists(configDir)
if !ok && err == nil {
err = os.Mkdir(configDir, 0755)
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")
}
return err
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 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)
}
+85 -40
View File
@@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"path/filepath"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
@@ -18,14 +19,28 @@ const (
launchdIdentifier = "com.cloudflare.cloudflared"
)
// OS-specific function for token file creation
var createTokenFile = createTokenFileUnix
func runApp(app *cli.App, _ chan struct{}) {
app.Commands = append(app.Commands, &cli.Command{
Name: "service",
Usage: "Manages the cloudflared launch agent",
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install cloudflared as an user launch agent",
Name: "install",
Usage: "Install cloudflared as an user launch agent",
ArgsUsage: "[TOKEN]",
Description: `
Installs cloudflared as a launchd-managed service.
A token may optionally be provided. If a token is provided, it will be written
to disk in the service configuration directory and the cloudflared service
configured to use it via the --token-file argument.
If no token is provided, cloudflared will run without the --token-file argument,
causing it to look for credentials in a configuration file upon startup.`,
Action: cliutil.ConfiguredAction(installLaunchd),
},
{
@@ -76,38 +91,45 @@ func isRootUser() bool {
return os.Geteuid() == 0
}
func installPath() (string, error) {
// User is root, use /Library/LaunchDaemons instead of home directory
func resolveLibraryPath(subPath, fileName string) (string, error) {
const libraryDirName = "Library"
// We use the system-wide /Library/... instead of ~/Library/... if the user is root
if isRootUser() {
return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", launchdIdentifier), nil
return filepath.Join("/", libraryDirName, subPath, fileName), nil
}
userHomeDir, err := userHomeDir()
// This returns the home dir of the executing user using OS-specific method
// for discovering the home dir. It's not recommended to call this when the
// user has root permission as $HOME depends on what options the user uses
// with sudo.
userHomeDir, err := homedir.Dir()
if err != nil {
return "", err
return "", errors.Wrap(err, "Cannot determine home directory for the user")
}
return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", userHomeDir, launchdIdentifier), nil
return filepath.Join(userHomeDir, libraryDirName, subPath, fileName), nil
}
// For docs on these subdirectories, see:
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html
func installPath() (string, error) {
subpath := "LaunchAgents"
if isRootUser() {
subpath = "LaunchDaemons"
}
return resolveLibraryPath(subpath, launchdIdentifier+".plist")
}
func stdoutPath() (string, error) {
if isRootUser() {
return fmt.Sprintf("/Library/Logs/%s.out.log", launchdIdentifier), nil
}
userHomeDir, err := userHomeDir()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/Library/Logs/%s.out.log", userHomeDir, launchdIdentifier), nil
return resolveLibraryPath("Logs", launchdIdentifier+".out.log")
}
func stderrPath() (string, error) {
if isRootUser() {
return fmt.Sprintf("/Library/Logs/%s.err.log", launchdIdentifier), nil
}
userHomeDir, err := userHomeDir()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/Library/Logs/%s.err.log", userHomeDir, launchdIdentifier), nil
return resolveLibraryPath("Logs", launchdIdentifier+".err.log")
}
func configPath() (string, error) {
return resolveLibraryPath("Application Support", launchdIdentifier)
}
func installLaunchd(c *cli.Context) error {
@@ -125,18 +147,45 @@ func installLaunchd(c *cli.Context) error {
etPath, err := os.Executable()
if err != nil {
log.Err(err).Msg("Error determining executable path")
return fmt.Errorf("Error determining executable path: %v", err)
return fmt.Errorf("error determining executable path: %w", err)
}
installPath, err := installPath()
if err != nil {
log.Err(err).Msg("Error determining install path")
return errors.Wrap(err, "Error determining install path")
}
extraArgs, err := getServiceExtraArgsFromCliArgs(c, log)
if err != nil {
errMsg := "Unable to determine extra arguments for launch daemon"
log.Err(err).Msg(errMsg)
return errors.Wrap(err, errMsg)
var extraArgs []string
if c.NArg() > 0 {
// The service has been installed using a token e.g.,
// $ cloudflared service install <token>
//
// Write the token file to a config directory so we can start the
// daemon with --token-file
// Don't use :=, if we did so we would create a new err variable and
// shadow the outer one, causing the defer below to not have access to
// the outer err
var cp string
cp, err = configPath()
if err != nil {
log.Err(err).Msg("Error determining path to config directory")
return err
}
// Ensure token file is removed if install fails at any point from now
// on
defer func() {
if err != nil {
removeTokenFile(cp, log)
}
}()
if err = writeTokenToConfigDir(c, cp); err != nil {
return fmt.Errorf("could not write token to configuration directory: %w", err)
}
extraArgs = buildArgsForTokenFile(cp)
}
stdoutPath, err := stdoutPath()
@@ -206,17 +255,13 @@ func uninstallLaunchd(c *cli.Context) error {
if err == nil {
log.Info().Msg("Launchd for cloudflared was uninstalled successfully")
}
return err
}
func userHomeDir() (string, error) {
// This returns the home dir of the executing user using OS-specific method
// for discovering the home dir. It's not recommended to call this function
// when the user has root permission as $HOME depends on what options the user
// use with sudo.
homeDir, err := homedir.Dir()
cp, err := configPath()
if err != nil {
return "", errors.Wrap(err, "Cannot determine home directory for the user")
log.Err(err).Msg("error determining path to config directory, not removing token file")
return err
}
return homeDir, nil
removeTokenFile(cp, log)
return nil
}
+8 -6
View File
@@ -33,6 +33,7 @@ import (
"github.com/cloudflare/cloudflared/diagnostic"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/features"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/management"
@@ -421,7 +422,7 @@ func StartServer(
// 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)
go runPrechecks(c, log, tunnelConfig.Region, tunnelConfig.ClientConfig.ConnectionFeaturesSnapshot().PostQuantum)
}
// Disable ICMP packet routing for quick tunnels
@@ -525,7 +526,7 @@ func StartServer(
// runPrechecks executes connectivity pre-checks and logs the results.
// Pre-checks are diagnostic only and do not gate tunnel startup.
func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
func runPrechecks(c *cli.Context, log *zerolog.Logger, region string, pqMode features.PostQuantumMode) {
ipVersion := allregions.Auto
if ipVersionStr := c.String(cfdflags.EdgeIpVersion); ipVersionStr != "" {
parsedVersion, err := parseConfigIPVersion(ipVersionStr)
@@ -537,9 +538,10 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
}
cfg := prechecks.Config{
Region: region,
IPVersion: ipVersion,
EdgeAddrs: c.StringSlice(cfdflags.Edge),
Region: region,
IPVersion: ipVersion,
EdgeAddrs: c.StringSlice(cfdflags.Edge),
ProtocolOverride: c.String(cfdflags.Protocol),
}
dialers := prechecks.RunDialers{
@@ -549,7 +551,7 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}},
}
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, pqMode, log, dialers)
// Output the human-readable table
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
+5 -15
View File
@@ -16,6 +16,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/cmd/cloudflared/inits"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
@@ -112,10 +113,10 @@ func CheckForUpdate(options updateOptions) (CheckResult, error) {
func encodeWindowsPath(path string) string {
// We do this because Windows allows spaces in directories such as
// Program Files but does not allow these directories to be spaced in batch files.
targetPath := strings.Replace(path, "Program Files (x86)", "PROGRA~2", -1)
targetPath := strings.ReplaceAll(path, "Program Files (x86)", "PROGRA~2")
// This is to do the same in 32 bit systems. We do this second so that the first
// replace is for x86 dirs.
targetPath = strings.Replace(targetPath, "Program Files", "PROGRA~1", -1)
targetPath = strings.ReplaceAll(targetPath, "Program Files", "PROGRA~1")
return targetPath
}
@@ -248,7 +249,7 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
if updateOutcome.Updated {
buildInfo.CloudflaredVersion = updateOutcome.Version
if IsSysV() {
if inits.IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.log.Info().Msg("Restarting service managed by SysV...")
pid, err := a.listeners.StartProcess()
@@ -298,16 +299,5 @@ func wasInstalledFromPackageManager() bool {
}
func isRunningFromTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
}
func IsSysV() bool {
if runtime.GOOS != "linux" {
return false
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
return true
return term.IsTerminal(int(os.Stdout.Fd())) // nolint:gosec
}
+171 -10
View File
@@ -8,6 +8,7 @@ package main
import (
"fmt"
"os"
"path/filepath"
"syscall"
"time"
"unsafe"
@@ -28,6 +29,12 @@ const (
windowsServiceDescription = "Cloudflared agent"
windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configure-tunnels/local-management/as-a-service/windows/"
// Env var that points to a directory for storing application-specific
// configuration and data (analogous to /etc/). Normally this points to
// C:\ProgramData.
programDataEnvVar = "PROGRAMDATA"
configDirName = "cloudflared"
recoverActionDelay = time.Second * 20
failureCountResetPeriod = time.Hour * 24
@@ -48,8 +55,18 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
Usage: "Manages the cloudflared Windows service",
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install cloudflared as a Windows service",
Name: "install",
Usage: "Install cloudflared as a Windows service",
ArgsUsage: "[TOKEN]",
Description: `
Installs cloudflared as a Windows service
A token may optionally be provided. If a token is provided, it will be written
to disk in the service configuration directory and the cloudflared service
configured to use it via the --token-file argument.
If no token is provided, cloudflared will run without the --token-file argument,
causing it to look for credentials in a configuration file upon startup.`,
Action: cliutil.ConfiguredAction(installWindowsService),
},
{
@@ -96,6 +113,105 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
}
}
// Creates the token file at the given path, restricting its permissions by
// modifying its Windows ACLs. We change the ACLs on the token file such that
// the Administrators group and SYSTEM account (which is what cloudflared runs
// as) have full access and all others are denied, with the Administrator group
// owning the file.
func createTokenFile(path string) error {
// This is a Windows Security Descriptor string describing the permissions
// we apply to the token file. This is the domain-specific language Windows
// uses for representing access rights.
//
// - O:BA -> Set the owner to the builtin administrators group (BA)
// - D: -> Start of discretionary access control list describing access rights
// - P -> Set the SE_DACL_PROTECTED flag, which prevents the file from
// inheriting the (usually permissive) ACEs from its parent directory
// - (A;;FA;;;BA) -> ACE #1: Allow (A) Full access (FA) to the Builtin Administrators group (BA)
// - (A;;FA;;;SY) -> ACE #2: Ditto but for the Local System user (SY)
//
// Relevant Docs:
//
// - SecurityDescriptor string as a whole:
// https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format
// - SID Strings such as BA/SY
// https://learn.microsoft.com/en-us/windows/win32/secauthz/sid-strings
// - ACE Strings such as (A;;FA;;BA)
// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings
const sdString = "O:BAD:P(A;;FA;;;BA)(A;;FA;;;SY)"
sd, err := windows.SecurityDescriptorFromString(sdString)
if err != nil {
return fmt.Errorf("create token security descriptor: %w", err)
}
pathRaw, err := windows.UTF16PtrFromString(path)
if err != nil {
return fmt.Errorf("convert path to UTF-16: %w", err)
}
f, err := windows.CreateFile(
pathRaw,
windows.GENERIC_WRITE,
0,
&windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: sd,
InheritHandle: 0,
},
windows.CREATE_ALWAYS, // Will truncate the file if it exists
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return fmt.Errorf("create token file: %w", err)
}
if err := windows.CloseHandle(f); err != nil {
return fmt.Errorf("close token file: %w", err)
}
// As with os.CreateFile / os.OpenFile on Unix, if the file already exists
// windows.CreateFile will not update the permission information, so we do
// that explicitly after creating the file.
owner, _, err := sd.Owner()
if err != nil {
return fmt.Errorf("get token file owner: %w", err)
}
dacl, _, err := sd.DACL()
if err != nil {
return fmt.Errorf("get token file DACL: %w", err)
}
// Bitmask indicating which security info we want to set on the file:
//
// OWNER_SECURITY_INFORMATION
// -> Set file owner
// DACL_SECURITY_INFORMATION
// -> Set ACEs
// PROTECTED_DACL_SECURITY_INFORMATION
// -> Update DACL to be "protected' such that it cannot inherit entries from its parent
const securityInfo = windows.OWNER_SECURITY_INFORMATION |
windows.DACL_SECURITY_INFORMATION |
windows.PROTECTED_DACL_SECURITY_INFORMATION
if err := windows.SetNamedSecurityInfo(
path,
windows.SE_FILE_OBJECT,
securityInfo,
owner,
nil,
dacl,
nil,
); err != nil {
return fmt.Errorf("set token file security info: %w", err)
}
return nil
}
type windowsService struct {
app *cli.App
graceShutdownC chan struct{}
@@ -173,6 +289,15 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
}
}
func getConfigDir() (string, error) {
progDat, progDatSet := os.LookupEnv(programDataEnvVar)
if !progDatSet {
return "", fmt.Errorf("could not find program data directory, %s env var must be set", programDataEnvVar)
}
return filepath.Join(progDat, configDirName), nil
}
func installWindowsService(c *cli.Context) error {
zeroLogger := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
@@ -192,11 +317,35 @@ func installWindowsService(c *cli.Context) error {
s.Close()
return errors.New(serviceAlreadyExistsWarn(windowsServiceName))
}
extraArgs, err := getServiceExtraArgsFromCliArgs(c, &log)
if err != nil {
errMsg := "Unable to determine extra arguments for windows service"
log.Err(err).Msg(errMsg)
return errors.Wrap(err, errMsg)
var extraArgs []string
if c.NArg() > 0 {
// The service has been installed using a token e.g.,
// $ cloudflared service install <token>
//
// Write the token file to a config directory so we can start the
// service with --token-file.
// Don't use :=, if we did so we would create a new err variable and
// shadow the outer one, causing the defer below to not have access to
// the outer err
var configDir string
configDir, err = getConfigDir()
if err != nil {
return fmt.Errorf("locate config dir: %w", err)
}
// Remove token file if service install fails any point onwards from here
defer func() {
if err != nil {
removeTokenFile(configDir, zeroLogger)
}
}()
if err = writeTokenToConfigDir(c, configDir); err != nil {
return fmt.Errorf("write token to configuration directory at %s: %w", configDir, err)
}
extraArgs = buildArgsForTokenFile(configDir)
}
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
@@ -219,10 +368,13 @@ func installWindowsService(c *cli.Context) error {
}
err = s.Start()
if err == nil {
log.Info().Msg("Agent service for cloudflared installed successfully")
if err != nil {
s.Delete()
return errors.Wrap(err, "Cannot start service")
}
return err
log.Info().Msg("Agent service for cloudflared installed successfully")
return nil
}
func uninstallWindowsService(c *cli.Context) error {
@@ -258,6 +410,15 @@ func uninstallWindowsService(c *cli.Context) error {
if err != nil {
return errors.Wrap(err, "Cannot remove event logger")
}
configDir, err := getConfigDir()
if err != nil {
// We don't need to hard-error out here, this isn't critical, but we should log it
log.Warn().Err(err).Msgf("Failed to find configuration directory, not removing secret token file")
} else {
removeTokenFile(configDir, &log)
}
return nil
}
+2 -2
View File
@@ -146,8 +146,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
case <-wsCtx.Done():
case <-r.Context().Done():
}
readPipe.Close()
writePipe.Close()
_ = readPipe.Close()
_ = writePipe.Close()
}()
originConn := &echoPipe{reader: readPipe, writer: writePipe}
+3 -18
View File
@@ -13,6 +13,7 @@ import (
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection/dialopts"
cfdquic "github.com/cloudflare/cloudflared/quic"
)
var (
@@ -29,7 +30,7 @@ func DialQuic(
connIndex uint8,
logger *zerolog.Logger,
opts dialopts.DialOpts,
) (quic.Connection, error) {
) (cfdquic.QUICConnection, error) {
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, opts, logger)
if err != nil {
return nil, err
@@ -43,11 +44,7 @@ func DialQuic(
}
// wrap the session, so that the UDPConn is closed after session is closed.
conn = &wrapCloseableConnQuicConnection{
conn,
udpConn,
}
return conn, nil
return cfdquic.NewQUICConnection(conn, udpConn)
}
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, opts dialopts.DialOpts, logger *zerolog.Logger) (*net.UDPConn, error) {
@@ -96,15 +93,3 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.Add
return udpConn, err
}
type wrapCloseableConnQuicConnection struct {
quic.Connection
udpConn *net.UDPConn
}
func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error {
err := w.Connection.CloseWithError(errorCode, reason)
_ = w.udpConn.Close()
return err
}
+4 -4
View File
@@ -41,7 +41,7 @@ const (
// quicConnection represents the type that facilitates Proxying via QUIC streams.
type quicConnection struct {
conn quic.Connection
conn cfdquic.QUICConnection
logger *zerolog.Logger
orchestrator Orchestrator
datagramHandler DatagramSessionHandler
@@ -54,10 +54,10 @@ type quicConnection struct {
gracePeriod time.Duration
}
// NewTunnelConnection takes a [quic.Connection] to wrap it for use with cloudflared application logic.
// NewTunnelConnection takes a [cfdquic.QUICConnection] to wrap it for use with cloudflared application logic.
func NewTunnelConnection(
ctx context.Context,
conn quic.Connection,
conn cfdquic.QUICConnection,
connIndex uint8,
orchestrator Orchestrator,
datagramSessionHandler DatagramSessionHandler,
@@ -169,7 +169,7 @@ func (q *quicConnection) acceptStream(ctx context.Context) error {
func (q *quicConnection) runStream(quicStream quic.Stream) {
ctx := quicStream.Context()
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
defer stream.Close()
defer func() { _ = stream.Close() }()
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
// code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream.
+6 -11
View File
@@ -8,9 +8,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
pkgerrors "github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -24,7 +22,6 @@ import (
"github.com/cloudflare/cloudflared/packet"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tracing"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
)
@@ -34,20 +31,18 @@ const (
demuxChanCapacity = 16
)
var (
errInvalidDestinationIP = errors.New("unable to parse destination IP")
)
var errInvalidDestinationIP = pkgerrors.New("unable to parse destination IP")
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
// connection streams.
type DatagramSessionHandler interface {
Serve(context.Context) error
pogs.SessionManager
tunnelpogs.SessionManager
}
type datagramV2Connection struct {
conn quic.Connection
conn cfdquic.QUICConnection
index uint8
// sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer
@@ -69,7 +64,7 @@ type datagramV2Connection struct {
}
func NewDatagramV2Connection(ctx context.Context,
conn quic.Connection,
conn cfdquic.QUICConnection,
originDialer ingress.OriginUDPDialer,
icmpRouter ingress.ICMPRouter,
index uint8,
@@ -166,7 +161,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
if err != nil {
originProxy.Close()
_ = originProxy.Close()
log.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).Msgf("Failed to register udp session")
tracing.EndWithErrorStatus(registerSpan, err)
q.flowLimiter.Release()
@@ -229,7 +224,7 @@ func (q *datagramV2Connection) closeUDPSession(ctx context.Context, sessionID uu
}
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
defer stream.Close()
defer func() { _ = stream.Close() }()
rpcClientStream, err := rpcquic.NewSessionClient(ctx, stream, q.rpcTimeout)
if err != nil {
// Log this at debug because this is not an error if session was closed due to lost connection
+9 -9
View File
@@ -7,12 +7,12 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/management"
cfdquic "github.com/cloudflare/cloudflared/quic/v3"
cfdquic "github.com/cloudflare/cloudflared/quic"
cfdquicv3 "github.com/cloudflare/cloudflared/quic/v3"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
@@ -22,20 +22,20 @@ var (
)
type datagramV3Connection struct {
conn quic.Connection
conn cfdquic.QUICConnection
index uint8
// datagramMuxer mux/demux datagrams from quic connection
datagramMuxer cfdquic.DatagramConn
metrics cfdquic.Metrics
datagramMuxer cfdquicv3.DatagramConn
metrics cfdquicv3.Metrics
logger *zerolog.Logger
}
func NewDatagramV3Connection(ctx context.Context,
conn quic.Connection,
sessionManager cfdquic.SessionManager,
conn cfdquic.QUICConnection,
sessionManager cfdquicv3.SessionManager,
icmpRouter ingress.ICMPRouter,
index uint8,
metrics cfdquic.Metrics,
metrics cfdquicv3.Metrics,
logger *zerolog.Logger,
) DatagramSessionHandler {
log := logger.
@@ -43,7 +43,7 @@ func NewDatagramV3Connection(ctx context.Context,
Int(management.EventTypeKey, int(management.UDP)).
Uint8(LogFieldConnIndex, index).
Logger()
datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
datagramMuxer := cfdquicv3.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
return &datagramV3Connection{
conn,
+2 -1
View File
@@ -18,6 +18,7 @@ import (
network "github.com/cloudflare/cloudflared/diagnostic/network"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/features"
"github.com/cloudflare/cloudflared/prechecks"
)
@@ -470,7 +471,7 @@ func collectPrechecks(region string) collectFunc {
}
emptyCert := ""
report := prechecks.Run(ctx, emptyCert, cfg, &log, dialers)
report := prechecks.Run(ctx, emptyCert, cfg, features.PostQuantumPrefer, &log, dialers)
// Write the report to a JSON file
// nolint: gosec
+13 -13
View File
@@ -10,7 +10,7 @@ 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.4
github.com/gobwas/ws v1.2.1
@@ -36,11 +36,11 @@ require (
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.51.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
golang.org/x/crypto v0.53.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.46.0
golang.org/x/term v0.44.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
@@ -91,13 +91,13 @@ require (
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/oauth2 v0.35.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-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.2 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/tools v0.47.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
)
+28 -28
View File
@@ -54,8 +54,8 @@ 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=
@@ -245,21 +245,21 @@ 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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -269,31 +269,31 @@ 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.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/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
+1 -1
View File
@@ -43,7 +43,7 @@ func (tc *tcpConnection) Stream(_ context.Context, tunnelConn io.ReadWriter, _ *
func (tc *tcpConnection) Write(b []byte) (int, error) {
if tc.writeTimeout > 0 {
if err := tc.Conn.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
if err := tc.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
tc.logger.Err(err).Msg("Error setting write deadline for TCP connection")
}
}
+23 -62
View File
@@ -13,7 +13,6 @@ import (
"time"
"github.com/gobwas/ws/wsutil"
gorillaWS "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/proxy"
@@ -61,7 +60,7 @@ func TestStreamTCPConnection(t *testing.T) {
})
errGroup.Go(func() error {
echoTCPOrigin(t, originConn)
originConn.Close()
_ = originConn.Close()
return nil
})
@@ -88,7 +87,7 @@ func TestDefaultStreamWSOverTCPConnection(t *testing.T) {
})
errGroup.Go(func() error {
echoTCPOrigin(t, originConn)
originConn.Close()
_ = originConn.Close()
return nil
})
@@ -117,14 +116,14 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
for _, status := range statusCodes {
handler := func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, []byte(sendMessage), body)
assert.NoError(t, err)
assert.Equal(t, []byte(sendMessage), body)
require.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
assert.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
w.Header().Set(echoHeaderName, echoHeaderReturnValue)
w.WriteHeader(status)
w.Write([]byte(echoMessage))
_, _ = w.Write([]byte(echoMessage))
}
origin := httptest.NewServer(http.HandlerFunc(handler))
defer origin.Close()
@@ -156,7 +155,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
errGroup.Go(func() error {
wsForwarderInConn, err := wsForwarderListener.Accept()
require.NoError(t, err)
defer wsForwarderInConn.Close()
defer func() { _ = wsForwarderInConn.Close() }()
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, TestLogger)
return nil
@@ -171,20 +170,22 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
// Request URL doesn't matter because the transport is using eyeballDialer to connectq
req, err := http.NewRequestWithContext(ctx, "GET", "http://test-socks-stream.com", bytes.NewBuffer([]byte(sendMessage)))
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = req.Body.Close() }()
req.Header.Set(echoHeaderName, echoHeaderIncomingValue)
resp, err := transport.RoundTrip(req)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, status, resp.StatusCode)
require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, []byte(echoMessage), body)
wsForwarderOutConn.Close()
edgeConn.Close()
tcpOverWSConn.Close()
_ = wsForwarderOutConn.Close()
_ = edgeConn.Close()
_ = tcpOverWSConn.Close()
require.NoError(t, errGroup.Wait())
}
@@ -205,7 +206,7 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
go func() {
time.Sleep(time.Millisecond * 10)
// Simulate losing connection to origin
originConn.Close()
_ = originConn.Close()
}()
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
tcpOverWSConn.Stream(ctx, eyeballConn, TestLogger)
@@ -221,11 +222,13 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
for i := 0; i < 50; i++ {
eyeballConn, edgeConn := net.Pipe()
req, err := http.NewRequestWithContext(ctx, http.MethodConnect, server.URL, edgeConn)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = req.Body.Close() }()
resp, err := client.Transport.RoundTrip(req)
assert.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer func() { _ = resp.Body.Close() }()
errGroup.Go(func() error {
for {
@@ -261,60 +264,18 @@ func echoWSEyeball(t *testing.T, conn net.Conn) {
assert.NoError(t, conn.Close())
}()
if !assert.NoError(t, wsutil.WriteClientBinary(conn, testMessage)) {
return
}
require.NoError(t, wsutil.WriteClientBinary(conn, testMessage))
readMsg, err := wsutil.ReadServerBinary(conn)
if !assert.NoError(t, err) {
return
}
require.NoError(t, err)
assert.Equal(t, testResponse, readMsg)
}
func echoWSOrigin(t *testing.T, expectMessages bool) *httptest.Server {
var upgrader = gorillaWS.Upgrader{
ReadBufferSize: 10,
WriteBufferSize: 10,
}
ws := func(w http.ResponseWriter, r *http.Request) {
header := make(http.Header)
for k, vs := range r.Header {
if k == "Test-Cloudflared-Echo" {
header[k] = vs
}
}
conn, err := upgrader.Upgrade(w, r, header)
require.NoError(t, err)
defer conn.Close()
sawMessage := false
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
if expectMessages && !sawMessage {
t.Errorf("unexpected error: %v", err)
}
return
}
assert.Equal(t, testMessage, p)
sawMessage = true
if err := conn.WriteMessage(messageType, testResponse); err != nil {
return
}
}
}
// NewTLSServer starts the server in another thread
return httptest.NewTLSServer(http.HandlerFunc(ws))
}
func echoTCPOrigin(t *testing.T, conn net.Conn) {
readBuffer := make([]byte, len(testMessage))
_, err := conn.Read(readBuffer)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, testMessage, readBuffer)
+7 -6
View File
@@ -17,12 +17,13 @@ import (
reflect "reflect"
time "time"
quic "github.com/quic-go/quic-go"
quic0 "github.com/quic-go/quic-go"
zerolog "github.com/rs/zerolog"
gomock "go.uber.org/mock/gomock"
dialopts "github.com/cloudflare/cloudflared/connection/dialopts"
allregions "github.com/cloudflare/cloudflared/edgediscovery/allregions"
quic "github.com/cloudflare/cloudflared/quic"
)
// MockDNSResolver is a mock of DNSResolver interface.
@@ -176,10 +177,10 @@ func (m *MockQUICDialer) EXPECT() *MockQUICDialerMockRecorder {
}
// DialQuic mocks base method.
func (m *MockQUICDialer) DialQuic(ctx context.Context, quicConfig *quic.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.Connection, error) {
func (m *MockQUICDialer) DialQuic(ctx context.Context, quicConfig *quic0.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.QUICConnection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DialQuic", ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
ret0, _ := ret[0].(quic.Connection)
ret0, _ := ret[0].(quic.QUICConnection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@@ -197,19 +198,19 @@ type MockQUICDialerDialQuicCall struct {
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICDialerDialQuicCall) Return(arg0 quic.Connection, arg1 error) *MockQUICDialerDialQuicCall {
func (c *MockQUICDialerDialQuicCall) Return(arg0 quic.QUICConnection, arg1 error) *MockQUICDialerDialQuicCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICDialerDialQuicCall) Do(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
func (c *MockQUICDialerDialQuicCall) Do(f func(context.Context, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, error)) *MockQUICDialerDialQuicCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, error)) *MockQUICDialerDialQuicCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
+55 -8
View File
@@ -12,6 +12,7 @@ import (
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/features"
)
const (
@@ -59,7 +60,10 @@ func (tr TransportResults) Collect() []CheckResult {
//
// Each failed probe is retried up to maxRetries times with exponential backoff.
// The suite is bounded by cfg.Timeout (defaultTimeout if zero).
func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, runDialers RunDialers) Report {
//
// pqMode controls the TLS curve preferences advertised during probe handshakes,
// matching the key-exchange algorithms used by the real tunnel connections.
func Run(ctx context.Context, caCert string, cfg Config, pqMode features.PostQuantumMode, log *zerolog.Logger, runDialers RunDialers) Report {
runID := uuid.New()
if cfg.Timeout <= 0 {
@@ -68,9 +72,10 @@ 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.
quicTLSConfig, quicTLSErr := probeTLSConfig(caCert, connection.QUIC)
http2TLSConfig, http2TLSErr := probeTLSConfig(caCert, connection.HTTP2)
// Build TLS configs once per protocol, applying the same curve preferences
// (including post-quantum curves) used by production tunnel connections.
quicTLSConfig, quicTLSErr := probeTLSConfig(caCert, connection.QUIC, pqMode)
http2TLSConfig, http2TLSErr := probeTLSConfig(caCert, connection.HTTP2, pqMode)
// 1) Resolve edge addresses. Each ResolvedTarget bundles its addr group
// with the DNS CheckResult that labels it, keeping the two in sync.
@@ -156,7 +161,7 @@ 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),
}
}
@@ -303,10 +308,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
+154 -14
View File
@@ -16,6 +16,7 @@ import (
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/features"
"github.com/cloudflare/cloudflared/mocks"
)
@@ -119,7 +120,7 @@ func TestRun_AllPass(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
@@ -150,7 +151,7 @@ func TestRun_QUICBlocked(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass + 2 QUIC Fail + 2 HTTP2 Pass + 1 API Pass.
requireStatuses(t, report, Pass, Pass, Fail, Fail, Pass, Pass, Pass)
@@ -180,7 +181,7 @@ func TestRun_HTTP2Blocked(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass + 2 QUIC Pass + 2 HTTP2 Fail + 1 API Pass.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Fail, Fail, Pass)
@@ -210,7 +211,7 @@ func TestRun_BothTransportsBlocked(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass + 2 QUIC Fail + 2 HTTP2 Fail + 1 API Pass.
requireStatuses(t, report, Pass, Pass, Fail, Fail, Fail, Fail, Pass)
@@ -249,7 +250,7 @@ func TestRun_PartialRegionQUICFail(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass + QUIC-region1 Pass + QUIC-region2 Fail + 2 HTTP2 Pass + 1 API Pass.
requireStatuses(t, report, Pass, Pass, Pass, Fail, Pass, Pass, Pass)
@@ -282,7 +283,7 @@ func TestRun_DNSFail_SkipsTransports(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// DNS failure emits 2 Fail rows (one per default region).
// Transport rows: one skip per DNS region for QUIC and HTTP/2 = 2 QUIC skips + 2 HTTP2 skips.
@@ -319,7 +320,7 @@ func TestRun_ManagementAPIFail(t *testing.T) {
Return(nil, errors.New("connection refused")).AnyTimes()
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass + 2 QUIC Pass + 2 HTTP2 Pass + 1 API Fail.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Fail)
@@ -350,7 +351,7 @@ func TestRun_RegionFlagForwardedToDNS(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Region: "us", Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// DNS rows carry regional hostnames (indices 0 and 1).
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[0].Target, "DNS region1")
@@ -388,7 +389,7 @@ func TestRun_QUICUsesProbeConnIndex(t *testing.T) {
Return(nopConn{}, nil)
Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
}
// TestRun_BothFamiliesProbed verifies that when both V4 and V6 addresses are
@@ -412,7 +413,7 @@ func TestRun_BothFamiliesProbed(t *testing.T) {
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results, all passing.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
@@ -454,7 +455,7 @@ func TestRun_IPVersionRestriction(t *testing.T) {
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})
features.PostQuantumPrefer, nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
})
@@ -489,7 +490,7 @@ func TestRun_EdgeAddrs_SingleAddr(t *testing.T) {
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
report := Run(t.Context(), emptyCert, cfg, features.PostQuantumPrefer, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 1 DNS Skip + 1 QUIC + 1 HTTP2 + 1 API = 4 results.
@@ -527,7 +528,7 @@ func TestRun_EdgeAddrs_MultipleAddrs(t *testing.T) {
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
report := Run(t.Context(), emptyCert, cfg, features.PostQuantumPrefer, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass (one per addr) + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
@@ -567,7 +568,7 @@ func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
report := Run(t.Context(), emptyCert, cfg, features.PostQuantumPrefer, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 1 DNS Fail + 1 QUIC Skip + 1 HTTP2 Skip + 1 API = 4 results.
@@ -579,3 +580,142 @@ func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
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)
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(&fakeQUICConn{}, nil).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, features.PostQuantumPrefer, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 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_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)
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(&fakeQUICConn{}, nil).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
ProtocolOverride: "quic",
}
report := Run(t.Context(), emptyCert, cfg, features.PostQuantumPrefer, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
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, features.PostQuantumPrefer, 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, features.PostQuantumPrefer, 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())
}
+15 -5
View File
@@ -15,8 +15,11 @@ import (
"github.com/cloudflare/cloudflared/connection/dialopts"
"github.com/cloudflare/cloudflared/connection"
cfdcrypto "github.com/cloudflare/cloudflared/crypto"
edgedial "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/features"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tlsconfig"
)
@@ -96,7 +99,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)
}
@@ -109,10 +112,13 @@ func (d *NetManagementDialer) DialContext(ctx context.Context, network, addr str
}
// probeTLSConfig builds a *tls.Config for a pre-check probe using the same
// certificate pool as the production tunnel. The SNI and NextProtos are taken from
// p.ProbeTLSSettings() so that the probe SNI is used instead of the production SNI,
// which avoids noisy logs in origintunneld.
func probeTLSConfig(caCert string, p connection.Protocol) (*tls.Config, error) {
// certificate pool and curve preferences as the production tunnel. The SNI and
// NextProtos are taken from p.ProbeTLSSettings() so that the probe SNI is used
// instead of the production SNI, which avoids noisy logs in origintunneld.
// Curve preferences are set via cfdcrypto.TLSConfigWithCurvePreferences so that
// prechecks advertise the same key-exchange algorithms (including post-quantum
// curves) as the real QUIC/H2 connections.
func probeTLSConfig(caCert string, p connection.Protocol, pqMode features.PostQuantumMode) (*tls.Config, error) {
settings := p.ProbeTLSSettings()
if settings == nil {
return nil, fmt.Errorf("no probe TLS settings for protocol %s", p)
@@ -124,6 +130,10 @@ func probeTLSConfig(caCert string, p connection.Protocol) (*tls.Config, error) {
if len(settings.NextProtos) > 0 {
cfg.NextProtos = settings.NextProtos
}
cfg, err = cfdcrypto.TLSConfigWithCurvePreferences(cfg, pqMode)
if err != nil {
return nil, fmt.Errorf("apply curve preferences: %w", err)
}
return cfg, nil
}
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection/dialopts"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
)
@@ -44,7 +45,7 @@ type QUICDialer interface {
connIndex uint8,
logger *zerolog.Logger,
opts dialopts.DialOpts,
) (quic.Connection, error)
) (cfdquic.QUICConnection, error)
}
// ManagementDialer abstracts the TCP dial to api.cloudflare.com:443 used by
+9
View File
@@ -126,4 +126,13 @@ type Config struct {
// no SRV records to validate — and transport probes target each addr
// individually, labeled with the original addr string.
EdgeAddrs []string
// ProtocolOverride is the raw --protocol flag value (e.g. "quic",
// "http2", "h2mux"). When non-empty and not "auto", the pre-checks still
// probe both transports for diagnostic completeness, but the reported
// SuggestedProtocol honours the override so that the summary message
// reflects what cloudflared will actually use — not what the probe
// heuristic would recommend on its own. Parsing happens inside the
// prechecks package.
ProtocolOverride string
}
+8 -8
View File
@@ -137,7 +137,7 @@ func (p *Proxy) ProxyHTTP(
p.proxyLocalRequest(originProxy, w, req, isWebsocket)
return nil
default:
return fmt.Errorf("Unrecognized service: %s, %t", rule.Service, originProxy)
return fmt.Errorf("unrecognized service: %s, %t", rule.Service, originProxy)
}
}
@@ -193,7 +193,7 @@ func (p *Proxy) proxyHTTPRequest(
) error {
roundTripReq := tr.Request
if isWebsocket {
roundTripReq = tr.Clone(tr.Request.Context())
roundTripReq = tr.Clone(tr.Context())
roundTripReq.Header.Set("Connection", "Upgrade")
roundTripReq.Header.Set("Upgrade", "websocket")
roundTripReq.Header.Set("Sec-Websocket-Version", "13")
@@ -203,7 +203,7 @@ func (p *Proxy) proxyHTTPRequest(
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if disableChunkedEncoding {
roundTripReq.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(tr.Request.Header.Get("Content-Length"))
cLength, err := strconv.Atoi(tr.Header.Get("Content-Length"))
if err == nil {
roundTripReq.ContentLength = int64(cLength)
}
@@ -228,7 +228,7 @@ func (p *Proxy) proxyHTTPRequest(
}
tracing.EndWithStatusCode(ttfbSpan, resp.StatusCode)
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
headers := make(http.Header, len(resp.Header))
// copy headers
@@ -249,11 +249,11 @@ func (p *Proxy) proxyHTTPRequest(
if !ok {
return errors.New("internal error: unsupported connection type")
}
defer rwc.Close()
defer func() { _ = rwc.Close() }()
eyeballStream := &bidirectionalStream{
writer: w,
reader: tr.Request.Body,
reader: tr.Body,
}
stream.Pipe(eyeballStream, rwc, logger)
@@ -292,7 +292,7 @@ func (p *Proxy) proxyStream(
return err
}
connectSpan.End()
defer originConn.Close()
defer func() { _ = originConn.Close() }()
logger.Debug().Msg("origin connection established")
encodedSpans := tr.GetSpans()
@@ -331,7 +331,7 @@ func (p *Proxy) proxyTCPStream(
return err
}
connectSpan.End()
defer originConn.Close()
defer func() { _ = originConn.Close() }()
logger.Debug().Msg("origin connection established")
encodedSpans := tr.GetSpans()
+10 -6
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/packet"
@@ -51,14 +50,14 @@ func (dm *DatagramMuxerV2) mtu() int {
}
type DatagramMuxerV2 struct {
session quic.Connection
session QUICConnection
logger *zerolog.Logger
sessionDemuxChan chan<- *packet.Session
packetDemuxChan chan Packet
}
func NewDatagramMuxerV2(
quicSession quic.Connection,
quicSession QUICConnection,
log *zerolog.Logger,
sessionDemuxChan chan<- *packet.Session,
) *DatagramMuxerV2 {
@@ -110,7 +109,8 @@ func (dm *DatagramMuxerV2) SendPacket(pk Packet) error {
return nil
}
// Demux reads datagrams from the QUIC connection and demuxes depending on whether it's a session or packet
// ServeReceive reads datagrams from the QUIC connection and demuxes them
// depending on whether it's a session or packet
func (dm *DatagramMuxerV2) ServeReceive(ctx context.Context) error {
for {
msg, err := dm.session.ReceiveDatagram(ctx)
@@ -144,8 +144,10 @@ func (dm *DatagramMuxerV2) demux(ctx context.Context, msgWithType []byte) error
switch msgType {
case DatagramTypeUDP:
return dm.handleSession(ctx, msg)
default:
case DatagramTypeIP, DatagramTypeIPWithTrace, DatagramTypeTracingSpan:
return dm.handlePacket(ctx, msg, msgType)
default:
return fmt.Errorf("unexpected datagram type %d", msgType)
}
}
@@ -189,8 +191,10 @@ func (dm *DatagramMuxerV2) handlePacket(ctx context.Context, pk []byte, msgType
Spans: spans,
TracingIdentity: tracingIdentity,
}
case DatagramTypeUDP:
return fmt.Errorf("unexpected datagram type %d in handlePacket", msgType)
default:
return fmt.Errorf("Unexpected datagram type %d", msgType)
return fmt.Errorf("unexpected datagram type %d", msgType)
}
select {
case <-ctx.Done():
+105
View File
@@ -0,0 +1,105 @@
package quic
import (
"context"
"errors"
"io"
"net"
"github.com/quic-go/quic-go"
)
// QUICConnection defines the subset of [quic.Connection] methods used by cloudflared.
// Consumers should accept this interface; producers should return [*ConnWithCloser].
type QUICConnection interface {
AcceptStream(ctx context.Context) (quic.Stream, error)
OpenStream() (quic.Stream, error)
OpenStreamSync(ctx context.Context) (quic.Stream, error)
CloseWithError(code quic.ApplicationErrorCode, reason string) error
Context() context.Context
SendDatagram(payload []byte) error
ReceiveDatagram(ctx context.Context) ([]byte, error)
LocalAddr() net.Addr
RemoteAddr() net.Addr
ConnectionState() quic.ConnectionState
}
// Compile-time assertion that *ConnWithCloser implements QUICConnection.
var _ QUICConnection = (*ConnWithCloser)(nil)
var (
// error returned when the [NewConnWithCloser] is called with a nil conn argument
ErrNilQuicConnection = errors.New("the provided quic connection is nil")
// error returned when the [NewConnWithCloser] is called with a nil closer argument
ErrNilCloser = errors.New("the provided closer is nil")
)
// ConnWithCloser wraps a [quic.Connection] and an [io.Closer] (typically the
// underlying [*net.UDPConn]). When [CloseWithError] is called the QUIC
// connection is closed first, then the closer is closed deterministically.
//
// A nil conn is only safe for [CloseWithError] (used in tests). All other
// delegated methods will panic on a nil conn.
type ConnWithCloser struct {
conn quic.Connection
closer io.Closer
}
// NewQUICConnection returns a [*ConnWithCloser] that will close closer after
// the QUIC connection is closed.
func NewQUICConnection(conn quic.Connection, closer io.Closer) (*ConnWithCloser, error) {
if conn == nil {
return nil, ErrNilQuicConnection
}
if closer == nil {
return nil, ErrNilCloser
}
return &ConnWithCloser{conn: conn, closer: closer}, nil
}
// CloseWithError closes the QUIC connection and then closes the underlying
// [io.Closer]. If both operations return errors, the errors are joined so that
// the closer error is no longer silently discarded.
func (c *ConnWithCloser) CloseWithError(code quic.ApplicationErrorCode, reason string) error {
connErr := c.conn.CloseWithError(code, reason)
closerErr := c.closer.Close()
return errors.Join(connErr, closerErr)
}
func (c *ConnWithCloser) AcceptStream(ctx context.Context) (quic.Stream, error) {
return c.conn.AcceptStream(ctx)
}
func (c *ConnWithCloser) OpenStream() (quic.Stream, error) {
return c.conn.OpenStream()
}
func (c *ConnWithCloser) OpenStreamSync(ctx context.Context) (quic.Stream, error) {
return c.conn.OpenStreamSync(ctx)
}
func (c *ConnWithCloser) Context() context.Context {
return c.conn.Context()
}
func (c *ConnWithCloser) SendDatagram(payload []byte) error {
return c.conn.SendDatagram(payload)
}
func (c *ConnWithCloser) ReceiveDatagram(ctx context.Context) ([]byte, error) {
return c.conn.ReceiveDatagram(ctx)
}
func (c *ConnWithCloser) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *ConnWithCloser) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *ConnWithCloser) ConnectionState() quic.ConnectionState {
return c.conn.ConnectionState()
}
+108
View File
@@ -0,0 +1,108 @@
package quic
import (
"errors"
"testing"
"github.com/quic-go/quic-go"
"github.com/stretchr/testify/require"
)
// mockCloser is an [io.Closer] that returns a configurable error.
type mockCloser struct {
closeErr error
}
func (m *mockCloser) Close() error {
return m.closeErr
}
// mockQuicConnection is a minimal test double for [quic.Connection].
type mockQuicConnection struct {
quic.Connection
closeWithErrorErr error
}
func (m *mockQuicConnection) CloseWithError(_ quic.ApplicationErrorCode, _ string) error {
return m.closeWithErrorErr
}
func TestNewConnWithCloser_NilConn(t *testing.T) {
t.Parallel()
conn, err := NewQUICConnection(nil, &mockCloser{})
require.ErrorIs(t, err, ErrNilQuicConnection)
require.Nil(t, conn)
}
func TestNewConnWithCloser_NilCloser(t *testing.T) {
t.Parallel()
conn, err := NewQUICConnection(&mockQuicConnection{}, nil)
require.ErrorIs(t, err, ErrNilCloser)
require.Nil(t, conn)
}
func TestNewConnWithCloser_Success(t *testing.T) {
t.Parallel()
qc := &mockQuicConnection{}
cl := &mockCloser{}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
require.NotNil(t, conn)
}
func TestConnWithCloser_CloseWithError_BothSucceed(t *testing.T) {
t.Parallel()
qc := &mockQuicConnection{}
cl := &mockCloser{}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.NoError(t, err)
}
func TestConnWithCloser_CloseWithError_QuicFails(t *testing.T) {
t.Parallel()
quicErr := errors.New("quic close failed")
qc := &mockQuicConnection{closeWithErrorErr: quicErr}
cl := &mockCloser{}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.ErrorIs(t, err, quicErr)
}
func TestConnWithCloser_CloseWithError_CloserFails(t *testing.T) {
t.Parallel()
closerErr := errors.New("closer failed")
qc := &mockQuicConnection{}
cl := &mockCloser{closeErr: closerErr}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.ErrorIs(t, err, closerErr)
}
func TestConnWithCloser_CloseWithError_BothFail(t *testing.T) {
t.Parallel()
quicErr := errors.New("quic close failed")
closerErr := errors.New("closer failed")
qc := &mockQuicConnection{closeWithErrorErr: quicErr}
cl := &mockCloser{closeErr: closerErr}
conn, err := NewQUICConnection(qc, cl)
require.NoError(t, err)
err = conn.CloseWithError(0, "test")
require.ErrorIs(t, err, quicErr)
require.ErrorIs(t, err, closerErr)
}
// TestConnWithCloser_ImplementsInterface is a runtime assertion that
// *ConnWithCloser satisfies QUICConnection. The compile-time assertion is in
// quic_connection.go.
func TestConnWithCloser_ImplementsInterface(t *testing.T) {
t.Parallel()
var _ QUICConnection = (*ConnWithCloser)(nil)
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"schedule:nonOfficeHours"
],
"enabledManagers": [
"dockerfile"
],
"dockerfile": {
"managerFilePatterns": [
"/(^|/)Dockerfile$/",
"/(^|/)Dockerfile\\.amd64$/",
"/(^|/)Dockerfile\\.arm64$/"
]
},
"packageRules": [
{
"description": "Disable updates for everything by default; only the distroless base image is managed for now",
"matchPackageNames": [
"*"
],
"enabled": false
},
{
"description": "Keep any distroless base image up to date by pinning and updating its digest, since tags like :nonroot are rolling tags without a semver version",
"matchManagers": [
"dockerfile"
],
"enabled": true,
"pinDigests": true,
"matchPackageNames": [
"gcr.io/distroless/{/,}**"
]
}
]
}
+4 -5
View File
@@ -67,7 +67,6 @@ func (s *bidirectionalStreamStatus) wait(maxWaitForSecondStream time.Duration) e
// Only wait for second stream to finish if maxWait is greater than zero
if maxWaitForSecondStream > 0 {
timer := time.NewTimer(maxWaitForSecondStream)
defer timer.Stop()
@@ -87,14 +86,14 @@ func (s *bidirectionalStreamStatus) isAnyDone() bool {
// Pipe copies copy data to & from provided io.ReadWriters.
func Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
_ = PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
}
// PipeBidirectional copies data two BidirectionStreams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.
// PipeBidirectional copies data to two unidirectional streams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.
// The main difference is that when piping data from a reader to a writer, if EOF is read, then this implementation propagates the EOF signal to the destination/writer by closing the write side of the
// Bidirectional Stream.
// Finally, depending on once EOF is ready from one of the provided streams, the other direction of streaming data will have a configured time period to also finish, otherwise,
// the method will return immediately with a timeout error. It is however, the responsability of the caller to close the associated streams in both ends in order to free all the resources/go-routines.
// the method will return immediately with a timeout error. It is however, the responsibility of the caller to close the associated streams in both ends in order to free all the resources/go-routines.
func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error {
status := newBiStreamStatus()
@@ -129,7 +128,7 @@ func unidirectionalStream(dst WriterCloser, src Reader, dir string, status *bidi
}
}()
defer dst.CloseWrite()
defer func() { _ = dst.CloseWrite() }()
_, err := copyData(dst, src, dir)
if err != nil {
+69 -2
View File
@@ -87,7 +87,7 @@ func main() {
// A good base middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.ClientIPFromRemoteAddr) // pick one ClientIPFrom* based on your infra, see below
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
@@ -221,6 +221,7 @@ type Router interface {
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Query(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
@@ -349,7 +350,11 @@ with `net/http` can be used with chi's mux.
| [Logger] | Logs the start and end of each request with the elapsed processing time |
| [NoCache] | Sets response headers to prevent clients from caching |
| [Profiler] | Easily attach net/http/pprof to your routers |
| [RealIP] | Sets a http.Request's RemoteAddr to either X-Real-IP or X-Forwarded-For |
| [ClientIPFromHeader] | Capture client IP from a trusted single-IP header (X-Real-IP, CF-Connecting-IP, ...) |
| [ClientIPFromXFF] | Capture client IP from X-Forwarded-For, skipping listed trusted CIDR prefixes |
| [ClientIPFromXFFTrustedProxies] | Capture client IP from X-Forwarded-For given a fixed number of trusted proxies |
| [ClientIPFromRemoteAddr] | Capture client IP from the TCP RemoteAddr (server directly on the public internet) |
| [RealIP] | Deprecated — vulnerable to IP spoofing; use [ClientIPFromXFF] or another ClientIPFrom\* middleware |
| [Recoverer] | Gracefully absorb panics and prints the stack trace |
| [RequestID] | Injects a request ID into the context of each request |
| [RedirectSlashes] | Redirect slashes on routing paths |
@@ -375,6 +380,12 @@ with `net/http` can be used with chi's mux.
[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger
[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache
[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler
[ClientIPFromHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromHeader
[ClientIPFromXFF]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFF
[ClientIPFromXFFTrustedProxies]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFFTrustedProxies
[ClientIPFromRemoteAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromRemoteAddr
[GetClientIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIP
[GetClientIPAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIPAddr
[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP
[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer
[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes
@@ -402,6 +413,62 @@ with `net/http` can be used with chi's mux.
[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts
[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter
### Choosing a ClientIP middleware
The legacy [RealIP] middleware is deprecated — it is vulnerable to IP spoofing
(GHSA-3fxj-6jh8-hvhx, GHSA-rjr7-jggh-pgcp, GHSA-9g5q-2w5x-hmxf) and mutates
`r.RemoteAddr`. Use one of the four `ClientIPFrom*` middlewares instead — pick
exactly one based on your network setup — and read the resulting IP with
[GetClientIP] (string) or [GetClientIPAddr] (`netip.Addr`):
| Your setup | Use |
|---|---|
| Directly on the public internet, no proxy | `middleware.ClientIPFromRemoteAddr` |
| Behind nginx (`X-Real-IP`), Cloudflare (`CF-Connecting-IP`), Apache (`X-Client-IP`) | `middleware.ClientIPFromHeader("<your-trusted-header>")` |
| Behind one or more proxies whose IP ranges you can list | `middleware.ClientIPFromXFF("10.0.0.0/8", ...)` |
| Behind a known, fixed number of proxies with dynamic IPs | `middleware.ClientIPFromXFFTrustedProxies(2)` |
```go
r := chi.NewRouter()
r.Use(middleware.RequestID)
// Pick exactly one. Examples for common deployments:
// Direct internet exposure (no proxy):
// r.Use(middleware.ClientIPFromRemoteAddr)
// Behind Cloudflare:
// r.Use(middleware.ClientIPFromHeader("CF-Connecting-IP"))
// Behind AWS CloudFront (or any proxy fleet with known CIDRs):
r.Use(middleware.ClientIPFromXFF(
"13.32.0.0/15", // CloudFront IPv4
"52.46.0.0/18", // CloudFront IPv4
"2600:9000::/28", // CloudFront IPv6
))
// Behind a known number of proxies with dynamic IPs:
// r.Use(middleware.ClientIPFromXFFTrustedProxies(2))
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
clientIP := middleware.GetClientIP(r.Context()) // for logs, rate-limit keys, etc.
_ = clientIP
})
```
These middlewares never mutate `r.RemoteAddr`. They store a normalized
`netip.Addr` in the request context — IPv4-mapped IPv6 (`::ffff:a.b.c.d`)
is folded to plain IPv4, and IPv6 zone identifiers carried in headers are
stripped, so one logical client maps to a single canonical key for logs,
rate limits, and ACLs.
See the per-function godoc for the full semantics of each middleware, and
[adam-p's "The perils of the 'real' client IP"](https://adam-p.ca/blog/2022/03/x-forwarded-for/)
for the underlying threat model.
### Extra middlewares & packages
Please see https://github.com/go-chi for additional packages.
+3 -2
View File
@@ -1,6 +1,6 @@
// Package chi is a small, idiomatic and composable router for building HTTP services.
//
// chi requires Go 1.14 or newer.
// chi supports the four most recent major versions of Go.
//
// Example:
//
@@ -77,7 +77,7 @@ type Router interface {
// path, with a fresh middleware stack for the inline-Router.
Group(fn func(r Router)) Router
// Route mounts a sub-Router along a `pattern`` string.
// Route mounts a sub-Router along a `pattern` string.
Route(pattern string, fn func(r Router)) Router
// Mount attaches another http.Handler along ./pattern/*
@@ -102,6 +102,7 @@ type Router interface {
Patch(pattern string, h http.HandlerFunc)
Post(pattern string, h http.HandlerFunc)
Put(pattern string, h http.HandlerFunc)
Query(pattern string, h http.HandlerFunc)
Trace(pattern string, h http.HandlerFunc)
// NotFound defines a handler to respond whenever a route could
+5 -4
View File
@@ -133,11 +133,12 @@ func (x *Context) RoutePattern() string {
return routePattern
}
// replaceWildcards takes a route pattern and recursively replaces all
// occurrences of "/*/" to "/".
// replaceWildcards takes a route pattern and replaces all occurrences of
// "/*/" with "/". It iteratively runs until no wildcards remain to
// correctly handle consecutive wildcards.
func replaceWildcards(p string) string {
if strings.Contains(p, "/*/") {
return replaceWildcards(strings.Replace(p, "/*/", "/", -1))
for strings.Contains(p, "/*/") {
p = strings.ReplaceAll(p, "/*/", "/")
}
return p
}
+14 -9
View File
@@ -107,7 +107,8 @@ func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
// Handle adds the route `pattern` that matches any http method to
// execute the `handler` http.Handler.
func (mx *Mux) Handle(pattern string, handler http.Handler) {
if method, rest, found := strings.Cut(pattern, " "); found {
if i := strings.IndexAny(pattern, " \t"); i >= 0 {
method, rest := pattern[:i], strings.TrimLeft(pattern[i+1:], " \t")
mx.Method(method, rest, handler)
return
}
@@ -118,12 +119,7 @@ func (mx *Mux) Handle(pattern string, handler http.Handler) {
// HandleFunc adds the route `pattern` that matches any http method to
// execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
if method, rest, found := strings.Cut(pattern, " "); found {
mx.Method(method, rest, handlerFn)
return
}
mx.handle(mALL, pattern, handlerFn)
mx.Handle(pattern, handlerFn)
}
// Method adds the route `pattern` that matches `method` http method to
@@ -190,6 +186,12 @@ func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) {
mx.handle(mPUT, pattern, handlerFn)
}
// Query adds the route `pattern` that matches a QUERY http method to
// execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) Query(pattern string, handlerFn http.HandlerFunc) {
mx.handle(mQUERY, pattern, handlerFn)
}
// Trace adds the route `pattern` that matches a TRACE http method to
// execute the `handlerFn` http.HandlerFunc.
func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) {
@@ -471,9 +473,12 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
// Find the route
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
if supportsPathValue {
setPathValue(rctx, r)
// Set http.Request path values from our request context
for i, key := range rctx.URLParams.Keys {
value := rctx.URLParams.Values[i]
r.SetPathValue(key, value)
}
r.Pattern = rctx.RoutePattern()
h.ServeHTTP(w, r)
return
-21
View File
@@ -1,21 +0,0 @@
//go:build go1.22 && !tinygo
// +build go1.22,!tinygo
package chi
import "net/http"
// supportsPathValue is true if the Go version is 1.22 and above.
//
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
const supportsPathValue = true
// setPathValue sets the path values in the Request value
// based on the provided request context.
func setPathValue(rctx *Context, r *http.Request) {
for i, key := range rctx.URLParams.Keys {
value := rctx.URLParams.Values[i]
r.SetPathValue(key, value)
}
}
-19
View File
@@ -1,19 +0,0 @@
//go:build !go1.22 || tinygo
// +build !go1.22 tinygo
package chi
import "net/http"
// supportsPathValue is true if the Go version is 1.22 and above.
//
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
const supportsPathValue = false
// setPathValue sets the path values in the Request value
// based on the provided request context.
//
// setPathValue is only supported in Go 1.22 and above so
// this is just a blank function so that it compiles.
func setPathValue(rctx *Context, r *http.Request) {
}
+27 -32
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"regexp"
"slices"
"sort"
"strconv"
"strings"
@@ -25,11 +26,17 @@ const (
mPATCH
mPOST
mPUT
mQUERY
mTRACE
)
var mALL = mCONNECT | mDELETE | mGET | mHEAD |
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
mOPTIONS | mPATCH | mPOST | mPUT | mQUERY | mTRACE
// methodQuery is the HTTP QUERY method (RFC 10008), a safe, idempotent
// method that conveys a request body. It is defined here until net/http
// provides an equivalent constant, at which point this is a 1-1 swap.
const methodQuery = "QUERY"
var methodMap = map[string]methodTyp{
http.MethodConnect: mCONNECT,
@@ -40,6 +47,7 @@ var methodMap = map[string]methodTyp{
http.MethodPatch: mPATCH,
http.MethodPost: mPOST,
http.MethodPut: mPUT,
methodQuery: mQUERY,
http.MethodTrace: mTRACE,
}
@@ -52,6 +60,7 @@ var reverseMethodMap = map[methodTyp]string{
mPATCH: http.MethodPatch,
mPOST: http.MethodPost,
mPUT: http.MethodPut,
mQUERY: methodQuery,
mTRACE: http.MethodTrace,
}
@@ -71,6 +80,7 @@ func RegisterMethod(method string) {
}
mt := methodTyp(2 << n)
methodMap[method] = mt
reverseMethodMap[mt] = method
mALL |= mt
}
@@ -328,7 +338,7 @@ func (n *node) replaceChild(label, tail byte, child *node) {
func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node {
nds := n.children[ntyp]
for i := 0; i < len(nds); i++ {
for i := range nds {
if nds[i].label == label && nds[i].tail == tail {
if ntyp == ntRegexp && nds[i].prefix != prefix {
continue
@@ -429,9 +439,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node {
}
// serially loop through each node grouped by the tail delimiter
for idx := 0; idx < len(nds); idx++ {
xn = nds[idx]
for _, xn = range nds {
// label for param nodes is the delimiter byte
p := strings.IndexByte(xsearch, xn.tail)
@@ -650,11 +658,9 @@ func (n *node) routes() []Route {
if h.handler == nil {
continue
}
m := methodTypString(mt)
if m == "" {
continue
if m, ok := reverseMethodMap[mt]; ok {
hs[m] = h.handler
}
hs[m] = h.handler
}
rt := Route{subroutes, hs, p}
@@ -772,29 +778,14 @@ func patParamKeys(pattern string) []string {
}
}
// longestPrefix finds the length of the shared prefix
// of two strings
func longestPrefix(k1, k2 string) int {
max := len(k1)
if l := len(k2); l < max {
max = l
}
var i int
for i = 0; i < max; i++ {
// longestPrefix finds the length of the shared prefix of two strings
func longestPrefix(k1, k2 string) (i int) {
for i = 0; i < min(len(k1), len(k2)); i++ {
if k1[i] != k2[i] {
break
}
}
return i
}
func methodTypString(method methodTyp) string {
for s, t := range methodMap {
if method == t {
return s
}
}
return ""
return
}
type nodes []*node
@@ -854,11 +845,15 @@ func Walk(r Routes, walkFn WalkFunc) error {
func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error {
for _, route := range r.Routes() {
mws := make([]func(http.Handler) http.Handler, len(parentMw))
copy(mws, parentMw)
mws = append(mws, r.Middlewares()...)
mws := slices.Concat(parentMw, r.Middlewares())
if route.SubRoutes != nil {
if handler, ok := route.Handlers["*"]; ok {
if chain, ok := handler.(*ChainHandler); ok {
mws = append(mws, chain.Middlewares...)
}
}
if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil {
return err
}
@@ -872,7 +867,7 @@ func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.H
}
fullRoute := parentRoute + route.Pattern
fullRoute = strings.Replace(fullRoute, "/*/", "/", -1)
fullRoute = strings.ReplaceAll(fullRoute, "/*/", "/")
if chain, ok := handler.(*ChainHandler); ok {
if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil {
-11
View File
@@ -1,11 +0,0 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.25
package blake2b
import "hash"
var _ hash.XOF = (*xof)(nil)
+3 -3
View File
@@ -20,7 +20,7 @@ func chacha20Poly1305Open(dst []byte, key []uint32, src, ad []byte) bool
func chacha20Poly1305Seal(dst []byte, key []uint32, src, ad []byte)
var (
useAVX2 = cpu.X86.HasAVX2 && cpu.X86.HasBMI2
useAVX2 = cpu.X86.HasSSSE3 && cpu.X86.HasAVX2 && cpu.X86.HasBMI2
)
// setupState writes a ChaCha20 input matrix to state. See
@@ -47,7 +47,7 @@ func setupState(state *[16]uint32, key *[32]byte, nonce []byte) {
}
func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []byte {
if !cpu.X86.HasSSSE3 {
if !useAVX2 {
return c.sealGeneric(dst, nonce, plaintext, additionalData)
}
@@ -66,7 +66,7 @@ func (c *chacha20poly1305) seal(dst, nonce, plaintext, additionalData []byte) []
}
func (c *chacha20poly1305) open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if !cpu.X86.HasSSSE3 {
if !useAVX2 {
return c.openGeneric(dst, nonce, ciphertext, additionalData)
}
File diff suppressed because it is too large Load Diff
+825
View File
@@ -0,0 +1,825 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cryptobyte
import (
encoding_asn1 "encoding/asn1"
"fmt"
"math/big"
"reflect"
"time"
"golang.org/x/crypto/cryptobyte/asn1"
)
// This file contains ASN.1-related methods for String and Builder.
// Builder
// AddASN1Int64 appends a DER-encoded ASN.1 INTEGER.
func (b *Builder) AddASN1Int64(v int64) {
b.addASN1Signed(asn1.INTEGER, v)
}
// AddASN1Int64WithTag appends a DER-encoded ASN.1 INTEGER with the
// given tag.
func (b *Builder) AddASN1Int64WithTag(v int64, tag asn1.Tag) {
b.addASN1Signed(tag, v)
}
// AddASN1Enum appends a DER-encoded ASN.1 ENUMERATION.
func (b *Builder) AddASN1Enum(v int64) {
b.addASN1Signed(asn1.ENUM, v)
}
func (b *Builder) addASN1Signed(tag asn1.Tag, v int64) {
b.AddASN1(tag, func(c *Builder) {
length := 1
for i := v; i >= 0x80 || i < -0x80; i >>= 8 {
length++
}
for ; length > 0; length-- {
i := v >> uint((length-1)*8) & 0xff
c.AddUint8(uint8(i))
}
})
}
// AddASN1Uint64 appends a DER-encoded ASN.1 INTEGER.
func (b *Builder) AddASN1Uint64(v uint64) {
b.AddASN1(asn1.INTEGER, func(c *Builder) {
length := 1
for i := v; i >= 0x80; i >>= 8 {
length++
}
for ; length > 0; length-- {
i := v >> uint((length-1)*8) & 0xff
c.AddUint8(uint8(i))
}
})
}
// AddASN1BigInt appends a DER-encoded ASN.1 INTEGER.
func (b *Builder) AddASN1BigInt(n *big.Int) {
if b.err != nil {
return
}
b.AddASN1(asn1.INTEGER, func(c *Builder) {
if n.Sign() < 0 {
// A negative number has to be converted to two's-complement form. So we
// invert and subtract 1. If the most-significant-bit isn't set then
// we'll need to pad the beginning with 0xff in order to keep the number
// negative.
nMinus1 := new(big.Int).Neg(n)
nMinus1.Sub(nMinus1, bigOne)
bytes := nMinus1.Bytes()
for i := range bytes {
bytes[i] ^= 0xff
}
if len(bytes) == 0 || bytes[0]&0x80 == 0 {
c.add(0xff)
}
c.add(bytes...)
} else if n.Sign() == 0 {
c.add(0)
} else {
bytes := n.Bytes()
if bytes[0]&0x80 != 0 {
c.add(0)
}
c.add(bytes...)
}
})
}
// AddASN1OctetString appends a DER-encoded ASN.1 OCTET STRING.
func (b *Builder) AddASN1OctetString(bytes []byte) {
b.AddASN1(asn1.OCTET_STRING, func(c *Builder) {
c.AddBytes(bytes)
})
}
const generalizedTimeFormatStr = "20060102150405Z0700"
// AddASN1GeneralizedTime appends a DER-encoded ASN.1 GENERALIZEDTIME.
func (b *Builder) AddASN1GeneralizedTime(t time.Time) {
if t.Year() < 0 || t.Year() > 9999 {
b.err = fmt.Errorf("cryptobyte: cannot represent %v as a GeneralizedTime", t)
return
}
b.AddASN1(asn1.GeneralizedTime, func(c *Builder) {
c.AddBytes([]byte(t.Format(generalizedTimeFormatStr)))
})
}
// AddASN1UTCTime appends a DER-encoded ASN.1 UTCTime.
func (b *Builder) AddASN1UTCTime(t time.Time) {
b.AddASN1(asn1.UTCTime, func(c *Builder) {
// As utilized by the X.509 profile, UTCTime can only
// represent the years 1950 through 2049.
if t.Year() < 1950 || t.Year() >= 2050 {
b.err = fmt.Errorf("cryptobyte: cannot represent %v as a UTCTime", t)
return
}
c.AddBytes([]byte(t.Format(defaultUTCTimeFormatStr)))
})
}
// AddASN1BitString appends a DER-encoded ASN.1 BIT STRING. This does not
// support BIT STRINGs that are not a whole number of bytes.
func (b *Builder) AddASN1BitString(data []byte) {
b.AddASN1(asn1.BIT_STRING, func(b *Builder) {
b.AddUint8(0)
b.AddBytes(data)
})
}
func (b *Builder) addBase128Int(n int64) {
var length int
if n == 0 {
length = 1
} else {
for i := n; i > 0; i >>= 7 {
length++
}
}
for i := length - 1; i >= 0; i-- {
o := byte(n >> uint(i*7))
o &= 0x7f
if i != 0 {
o |= 0x80
}
b.add(o)
}
}
func isValidOID(oid encoding_asn1.ObjectIdentifier) bool {
if len(oid) < 2 {
return false
}
if oid[0] > 2 || (oid[0] <= 1 && oid[1] >= 40) {
return false
}
for _, v := range oid {
if v < 0 {
return false
}
}
return true
}
func (b *Builder) AddASN1ObjectIdentifier(oid encoding_asn1.ObjectIdentifier) {
b.AddASN1(asn1.OBJECT_IDENTIFIER, func(b *Builder) {
if !isValidOID(oid) {
b.err = fmt.Errorf("cryptobyte: invalid OID: %v", oid)
return
}
b.addBase128Int(int64(oid[0])*40 + int64(oid[1]))
for _, v := range oid[2:] {
b.addBase128Int(int64(v))
}
})
}
func (b *Builder) AddASN1Boolean(v bool) {
b.AddASN1(asn1.BOOLEAN, func(b *Builder) {
if v {
b.AddUint8(0xff)
} else {
b.AddUint8(0)
}
})
}
func (b *Builder) AddASN1NULL() {
b.add(uint8(asn1.NULL), 0)
}
// MarshalASN1 calls encoding_asn1.Marshal on its input and appends the result if
// successful or records an error if one occurred.
func (b *Builder) MarshalASN1(v interface{}) {
// NOTE(martinkr): This is somewhat of a hack to allow propagation of
// encoding_asn1.Marshal errors into Builder.err. N.B. if you call MarshalASN1 with a
// value embedded into a struct, its tag information is lost.
if b.err != nil {
return
}
bytes, err := encoding_asn1.Marshal(v)
if err != nil {
b.err = err
return
}
b.AddBytes(bytes)
}
// AddASN1 appends an ASN.1 object. The object is prefixed with the given tag.
// Tags greater than 30 are not supported and result in an error (i.e.
// low-tag-number form only). The child builder passed to the
// BuilderContinuation can be used to build the content of the ASN.1 object.
func (b *Builder) AddASN1(tag asn1.Tag, f BuilderContinuation) {
if b.err != nil {
return
}
// Identifiers with the low five bits set indicate high-tag-number format
// (two or more octets), which we don't support.
if tag&0x1f == 0x1f {
b.err = fmt.Errorf("cryptobyte: high-tag number identifier octets not supported: 0x%x", tag)
return
}
b.AddUint8(uint8(tag))
b.addLengthPrefixed(1, true, f)
}
// String
// ReadASN1Boolean decodes an ASN.1 BOOLEAN and converts it to a boolean
// representation into out and advances. It reports whether the read
// was successful.
func (s *String) ReadASN1Boolean(out *bool) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.BOOLEAN) || len(bytes) != 1 {
return false
}
switch bytes[0] {
case 0:
*out = false
case 0xff:
*out = true
default:
return false
}
return true
}
// ReadASN1Integer decodes an ASN.1 INTEGER into out and advances. If out does
// not point to an integer, to a big.Int, or to a []byte it panics. Only
// positive and zero values can be decoded into []byte, and they are returned as
// big-endian binary values that share memory with s. Positive values will have
// no leading zeroes, and zero will be returned as a single zero byte.
// ReadASN1Integer reports whether the read was successful.
func (s *String) ReadASN1Integer(out interface{}) bool {
switch out := out.(type) {
case *int, *int8, *int16, *int32, *int64:
var i int64
if !s.readASN1Int64(&i) || reflect.ValueOf(out).Elem().OverflowInt(i) {
return false
}
reflect.ValueOf(out).Elem().SetInt(i)
return true
case *uint, *uint8, *uint16, *uint32, *uint64:
var u uint64
if !s.readASN1Uint64(&u) || reflect.ValueOf(out).Elem().OverflowUint(u) {
return false
}
reflect.ValueOf(out).Elem().SetUint(u)
return true
case *big.Int:
return s.readASN1BigInt(out)
case *[]byte:
return s.readASN1Bytes(out)
default:
panic("out does not point to an integer type")
}
}
func checkASN1Integer(bytes []byte) bool {
if len(bytes) == 0 {
// An INTEGER is encoded with at least one octet.
return false
}
if len(bytes) == 1 {
return true
}
if bytes[0] == 0 && bytes[1]&0x80 == 0 || bytes[0] == 0xff && bytes[1]&0x80 == 0x80 {
// Value is not minimally encoded.
return false
}
return true
}
var bigOne = big.NewInt(1)
func (s *String) readASN1BigInt(out *big.Int) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.INTEGER) || !checkASN1Integer(bytes) {
return false
}
if bytes[0]&0x80 == 0x80 {
// Negative number.
neg := make([]byte, len(bytes))
for i, b := range bytes {
neg[i] = ^b
}
out.SetBytes(neg)
out.Add(out, bigOne)
out.Neg(out)
} else {
out.SetBytes(bytes)
}
return true
}
func (s *String) readASN1Bytes(out *[]byte) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.INTEGER) || !checkASN1Integer(bytes) {
return false
}
if bytes[0]&0x80 == 0x80 {
return false
}
for len(bytes) > 1 && bytes[0] == 0 {
bytes = bytes[1:]
}
*out = bytes
return true
}
func (s *String) readASN1Int64(out *int64) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.INTEGER) || !checkASN1Integer(bytes) || !asn1Signed(out, bytes) {
return false
}
return true
}
func asn1Signed(out *int64, n []byte) bool {
length := len(n)
if length > 8 {
return false
}
for i := 0; i < length; i++ {
*out <<= 8
*out |= int64(n[i])
}
// Shift up and down in order to sign extend the result.
*out <<= 64 - uint8(length)*8
*out >>= 64 - uint8(length)*8
return true
}
func (s *String) readASN1Uint64(out *uint64) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.INTEGER) || !checkASN1Integer(bytes) || !asn1Unsigned(out, bytes) {
return false
}
return true
}
func asn1Unsigned(out *uint64, n []byte) bool {
length := len(n)
if length > 9 || length == 9 && n[0] != 0 {
// Too large for uint64.
return false
}
if n[0]&0x80 != 0 {
// Negative number.
return false
}
for i := 0; i < length; i++ {
*out <<= 8
*out |= uint64(n[i])
}
return true
}
// ReadASN1Int64WithTag decodes an ASN.1 INTEGER with the given tag into out
// and advances. It reports whether the read was successful and resulted in a
// value that can be represented in an int64.
func (s *String) ReadASN1Int64WithTag(out *int64, tag asn1.Tag) bool {
var bytes String
return s.ReadASN1(&bytes, tag) && checkASN1Integer(bytes) && asn1Signed(out, bytes)
}
// ReadASN1Enum decodes an ASN.1 ENUMERATION into out and advances. It reports
// whether the read was successful.
func (s *String) ReadASN1Enum(out *int) bool {
var bytes String
var i int64
if !s.ReadASN1(&bytes, asn1.ENUM) || !checkASN1Integer(bytes) || !asn1Signed(&i, bytes) {
return false
}
if int64(int(i)) != i {
return false
}
*out = int(i)
return true
}
func (s *String) readBase128Int(out *int) bool {
ret := 0
for i := 0; len(*s) > 0; i++ {
if i == 5 {
return false
}
// Avoid overflowing int on a 32-bit platform.
// We don't want different behavior based on the architecture.
if ret >= 1<<(31-7) {
return false
}
ret <<= 7
b := s.read(1)[0]
// ITU-T X.690, section 8.19.2:
// The subidentifier shall be encoded in the fewest possible octets,
// that is, the leading octet of the subidentifier shall not have the value 0x80.
if i == 0 && b == 0x80 {
return false
}
ret |= int(b & 0x7f)
if b&0x80 == 0 {
*out = ret
return true
}
}
return false // truncated
}
// ReadASN1ObjectIdentifier decodes an ASN.1 OBJECT IDENTIFIER into out and
// advances. It reports whether the read was successful.
func (s *String) ReadASN1ObjectIdentifier(out *encoding_asn1.ObjectIdentifier) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.OBJECT_IDENTIFIER) || len(bytes) == 0 {
return false
}
// In the worst case, we get two elements from the first byte (which is
// encoded differently) and then every varint is a single byte long.
components := make([]int, len(bytes)+1)
// The first varint is 40*value1 + value2:
// According to this packing, value1 can take the values 0, 1 and 2 only.
// When value1 = 0 or value1 = 1, then value2 is <= 39. When value1 = 2,
// then there are no restrictions on value2.
var v int
if !bytes.readBase128Int(&v) {
return false
}
if v < 80 {
components[0] = v / 40
components[1] = v % 40
} else {
components[0] = 2
components[1] = v - 80
}
i := 2
for ; len(bytes) > 0; i++ {
if !bytes.readBase128Int(&v) {
return false
}
components[i] = v
}
*out = components[:i]
return true
}
// ReadASN1GeneralizedTime decodes an ASN.1 GENERALIZEDTIME into out and
// advances. It reports whether the read was successful.
func (s *String) ReadASN1GeneralizedTime(out *time.Time) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.GeneralizedTime) {
return false
}
t := string(bytes)
res, err := time.Parse(generalizedTimeFormatStr, t)
if err != nil {
return false
}
if serialized := res.Format(generalizedTimeFormatStr); serialized != t {
return false
}
*out = res
return true
}
const defaultUTCTimeFormatStr = "060102150405Z0700"
// ReadASN1UTCTime decodes an ASN.1 UTCTime into out and advances.
// It reports whether the read was successful.
func (s *String) ReadASN1UTCTime(out *time.Time) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.UTCTime) {
return false
}
t := string(bytes)
formatStr := defaultUTCTimeFormatStr
var err error
res, err := time.Parse(formatStr, t)
if err != nil {
// Fallback to minute precision if we can't parse second
// precision. If we are following X.509 or X.690 we shouldn't
// support this, but we do.
formatStr = "0601021504Z0700"
res, err = time.Parse(formatStr, t)
}
if err != nil {
return false
}
if serialized := res.Format(formatStr); serialized != t {
return false
}
if res.Year() >= 2050 {
// UTCTime interprets the low order digits 50-99 as 1950-99.
// This only applies to its use in the X.509 profile.
// See https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
res = res.AddDate(-100, 0, 0)
}
*out = res
return true
}
// ReadASN1BitString decodes an ASN.1 BIT STRING into out and advances.
// It reports whether the read was successful.
func (s *String) ReadASN1BitString(out *encoding_asn1.BitString) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.BIT_STRING) || len(bytes) == 0 ||
len(bytes)*8/8 != len(bytes) {
return false
}
paddingBits := bytes[0]
bytes = bytes[1:]
if paddingBits > 7 ||
len(bytes) == 0 && paddingBits != 0 ||
len(bytes) > 0 && bytes[len(bytes)-1]&(1<<paddingBits-1) != 0 {
return false
}
out.BitLength = len(bytes)*8 - int(paddingBits)
out.Bytes = bytes
return true
}
// ReadASN1BitStringAsBytes decodes an ASN.1 BIT STRING into out and advances. It is
// an error if the BIT STRING is not a whole number of bytes. It reports
// whether the read was successful.
func (s *String) ReadASN1BitStringAsBytes(out *[]byte) bool {
var bytes String
if !s.ReadASN1(&bytes, asn1.BIT_STRING) || len(bytes) == 0 {
return false
}
paddingBits := bytes[0]
if paddingBits != 0 {
return false
}
*out = bytes[1:]
return true
}
// ReadASN1Bytes reads the contents of a DER-encoded ASN.1 element (not including
// tag and length bytes) into out, and advances. The element must match the
// given tag. It reports whether the read was successful.
func (s *String) ReadASN1Bytes(out *[]byte, tag asn1.Tag) bool {
return s.ReadASN1((*String)(out), tag)
}
// ReadASN1 reads the contents of a DER-encoded ASN.1 element (not including
// tag and length bytes) into out, and advances. The element must match the
// given tag. It reports whether the read was successful.
//
// Tags greater than 30 are not supported (i.e. low-tag-number format only).
func (s *String) ReadASN1(out *String, tag asn1.Tag) bool {
var t asn1.Tag
if !s.ReadAnyASN1(out, &t) || t != tag {
return false
}
return true
}
// ReadASN1Element reads the contents of a DER-encoded ASN.1 element (including
// tag and length bytes) into out, and advances. The element must match the
// given tag. It reports whether the read was successful.
//
// Tags greater than 30 are not supported (i.e. low-tag-number format only).
func (s *String) ReadASN1Element(out *String, tag asn1.Tag) bool {
var t asn1.Tag
if !s.ReadAnyASN1Element(out, &t) || t != tag {
return false
}
return true
}
// ReadAnyASN1 reads the contents of a DER-encoded ASN.1 element (not including
// tag and length bytes) into out, sets outTag to its tag, and advances.
// It reports whether the read was successful.
//
// Tags greater than 30 are not supported (i.e. low-tag-number format only).
func (s *String) ReadAnyASN1(out *String, outTag *asn1.Tag) bool {
return s.readASN1(out, outTag, true /* skip header */)
}
// ReadAnyASN1Element reads the contents of a DER-encoded ASN.1 element
// (including tag and length bytes) into out, sets outTag to is tag, and
// advances. It reports whether the read was successful.
//
// Tags greater than 30 are not supported (i.e. low-tag-number format only).
func (s *String) ReadAnyASN1Element(out *String, outTag *asn1.Tag) bool {
return s.readASN1(out, outTag, false /* include header */)
}
// PeekASN1Tag reports whether the next ASN.1 value on the string starts with
// the given tag.
func (s String) PeekASN1Tag(tag asn1.Tag) bool {
if len(s) == 0 {
return false
}
return asn1.Tag(s[0]) == tag
}
// SkipASN1 reads and discards an ASN.1 element with the given tag. It
// reports whether the operation was successful.
func (s *String) SkipASN1(tag asn1.Tag) bool {
var unused String
return s.ReadASN1(&unused, tag)
}
// ReadOptionalASN1 attempts to read the contents of a DER-encoded ASN.1
// element (not including tag and length bytes) tagged with the given tag into
// out. It stores whether an element with the tag was found in outPresent,
// unless outPresent is nil. It reports whether the read was successful.
func (s *String) ReadOptionalASN1(out *String, outPresent *bool, tag asn1.Tag) bool {
present := s.PeekASN1Tag(tag)
if outPresent != nil {
*outPresent = present
}
if present && !s.ReadASN1(out, tag) {
return false
}
return true
}
// SkipOptionalASN1 advances s over an ASN.1 element with the given tag, or
// else leaves s unchanged. It reports whether the operation was successful.
func (s *String) SkipOptionalASN1(tag asn1.Tag) bool {
if !s.PeekASN1Tag(tag) {
return true
}
var unused String
return s.ReadASN1(&unused, tag)
}
// ReadOptionalASN1Integer attempts to read an optional ASN.1 INTEGER explicitly
// tagged with tag into out and advances. If no element with a matching tag is
// present, it writes defaultValue into out instead. Otherwise, it behaves like
// ReadASN1Integer.
func (s *String) ReadOptionalASN1Integer(out interface{}, tag asn1.Tag, defaultValue interface{}) bool {
var present bool
var i String
if !s.ReadOptionalASN1(&i, &present, tag) {
return false
}
if !present {
switch out.(type) {
case *int, *int8, *int16, *int32, *int64,
*uint, *uint8, *uint16, *uint32, *uint64, *[]byte:
reflect.ValueOf(out).Elem().Set(reflect.ValueOf(defaultValue))
case *big.Int:
if defaultValue, ok := defaultValue.(*big.Int); ok {
out.(*big.Int).Set(defaultValue)
} else {
panic("out points to big.Int, but defaultValue does not")
}
default:
panic("invalid integer type")
}
return true
}
if !i.ReadASN1Integer(out) || !i.Empty() {
return false
}
return true
}
// ReadOptionalASN1OctetString attempts to read an optional ASN.1 OCTET STRING
// explicitly tagged with tag into out and advances. If no element with a
// matching tag is present, it sets "out" to nil instead. It reports
// whether the read was successful.
func (s *String) ReadOptionalASN1OctetString(out *[]byte, outPresent *bool, tag asn1.Tag) bool {
var present bool
var child String
if !s.ReadOptionalASN1(&child, &present, tag) {
return false
}
if outPresent != nil {
*outPresent = present
}
if present {
var oct String
if !child.ReadASN1(&oct, asn1.OCTET_STRING) || !child.Empty() {
return false
}
*out = oct
} else {
*out = nil
}
return true
}
// ReadOptionalASN1Boolean attempts to read an optional ASN.1 BOOLEAN
// explicitly tagged with tag into out and advances. If no element with a
// matching tag is present, it sets "out" to defaultValue instead. It reports
// whether the read was successful.
func (s *String) ReadOptionalASN1Boolean(out *bool, tag asn1.Tag, defaultValue bool) bool {
var present bool
var child String
if !s.ReadOptionalASN1(&child, &present, tag) {
return false
}
if !present {
*out = defaultValue
return true
}
return child.ReadASN1Boolean(out)
}
func (s *String) readASN1(out *String, outTag *asn1.Tag, skipHeader bool) bool {
if len(*s) < 2 {
return false
}
tag, lenByte := (*s)[0], (*s)[1]
if tag&0x1f == 0x1f {
// ITU-T X.690 section 8.1.2
//
// An identifier octet with a tag part of 0x1f indicates a high-tag-number
// form identifier with two or more octets. We only support tags less than
// 31 (i.e. low-tag-number form, single octet identifier).
return false
}
if outTag != nil {
*outTag = asn1.Tag(tag)
}
// ITU-T X.690 section 8.1.3
//
// Bit 8 of the first length byte indicates whether the length is short- or
// long-form.
var length, headerLen uint32 // length includes headerLen
if lenByte&0x80 == 0 {
// Short-form length (section 8.1.3.4), encoded in bits 1-7.
length = uint32(lenByte) + 2
headerLen = 2
} else {
// Long-form length (section 8.1.3.5). Bits 1-7 encode the number of octets
// used to encode the length.
lenLen := lenByte & 0x7f
var len32 uint32
if lenLen == 0 || lenLen > 4 || len(*s) < int(2+lenLen) {
return false
}
lenBytes := String((*s)[2 : 2+lenLen])
if !lenBytes.readUnsigned(&len32, int(lenLen)) {
return false
}
// ITU-T X.690 section 10.1 (DER length forms) requires encoding the length
// with the minimum number of octets.
if len32 < 128 {
// Length should have used short-form encoding.
return false
}
if len32>>((lenLen-1)*8) == 0 {
// Leading octet is 0. Length should have been at least one byte shorter.
return false
}
headerLen = 2 + uint32(lenLen)
if headerLen+len32 < len32 {
// Overflow.
return false
}
length = headerLen + len32
}
if int(length) < 0 || !s.ReadBytes((*[]byte)(out), int(length)) {
return false
}
if skipHeader && !out.Skip(int(headerLen)) {
panic("cryptobyte: internal error")
}
return true
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package asn1 contains supporting types for parsing and building ASN.1
// messages with the cryptobyte package.
package asn1
// Tag represents an ASN.1 identifier octet, consisting of a tag number
// (indicating a type) and class (such as context-specific or constructed).
//
// Methods in the cryptobyte package only support the low-tag-number form, i.e.
// a single identifier octet with bits 7-8 encoding the class and bits 1-6
// encoding the tag number.
type Tag uint8
const (
classConstructed = 0x20
classContextSpecific = 0x80
)
// Constructed returns t with the constructed class bit set.
func (t Tag) Constructed() Tag { return t | classConstructed }
// ContextSpecific returns t with the context-specific class bit set.
func (t Tag) ContextSpecific() Tag { return t | classContextSpecific }
// The following is a list of standard tag and class combinations.
const (
BOOLEAN = Tag(1)
INTEGER = Tag(2)
BIT_STRING = Tag(3)
OCTET_STRING = Tag(4)
NULL = Tag(5)
OBJECT_IDENTIFIER = Tag(6)
ENUM = Tag(10)
UTF8String = Tag(12)
SEQUENCE = Tag(16 | classConstructed)
SET = Tag(17 | classConstructed)
PrintableString = Tag(19)
T61String = Tag(20)
IA5String = Tag(22)
UTCTime = Tag(23)
GeneralizedTime = Tag(24)
GeneralString = Tag(27)
)
+350
View File
@@ -0,0 +1,350 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cryptobyte
import (
"errors"
"fmt"
)
// A Builder builds byte strings from fixed-length and length-prefixed values.
// Builders either allocate space as needed, or are fixed, which means that
// they write into a given buffer and produce an error if it's exhausted.
//
// The zero value is a usable Builder that allocates space as needed.
//
// Simple values are marshaled and appended to a Builder using methods on the
// Builder. Length-prefixed values are marshaled by providing a
// BuilderContinuation, which is a function that writes the inner contents of
// the value to a given Builder. See the documentation for BuilderContinuation
// for details.
type Builder struct {
err error
result []byte
fixedSize bool
child *Builder
offset int
pendingLenLen int
pendingIsASN1 bool
inContinuation *bool
}
// NewBuilder creates a Builder that appends its output to the given buffer.
// Like append(), the slice will be reallocated if its capacity is exceeded.
// Use Bytes to get the final buffer.
func NewBuilder(buffer []byte) *Builder {
return &Builder{
result: buffer,
}
}
// NewFixedBuilder creates a Builder that appends its output into the given
// buffer. This builder does not reallocate the output buffer. Writes that
// would exceed the buffer's capacity are treated as an error.
func NewFixedBuilder(buffer []byte) *Builder {
return &Builder{
result: buffer,
fixedSize: true,
}
}
// SetError sets the value to be returned as the error from Bytes. Writes
// performed after calling SetError are ignored.
func (b *Builder) SetError(err error) {
b.err = err
}
// Bytes returns the bytes written by the builder or an error if one has
// occurred during building.
func (b *Builder) Bytes() ([]byte, error) {
if b.err != nil {
return nil, b.err
}
return b.result[b.offset:], nil
}
// BytesOrPanic returns the bytes written by the builder or panics if an error
// has occurred during building.
func (b *Builder) BytesOrPanic() []byte {
if b.err != nil {
panic(b.err)
}
return b.result[b.offset:]
}
// AddUint8 appends an 8-bit value to the byte string.
func (b *Builder) AddUint8(v uint8) {
b.add(byte(v))
}
// AddUint16 appends a big-endian, 16-bit value to the byte string.
func (b *Builder) AddUint16(v uint16) {
b.add(byte(v>>8), byte(v))
}
// AddUint24 appends a big-endian, 24-bit value to the byte string. The highest
// byte of the 32-bit input value is silently truncated.
func (b *Builder) AddUint24(v uint32) {
b.add(byte(v>>16), byte(v>>8), byte(v))
}
// AddUint32 appends a big-endian, 32-bit value to the byte string.
func (b *Builder) AddUint32(v uint32) {
b.add(byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
// AddUint48 appends a big-endian, 48-bit value to the byte string.
func (b *Builder) AddUint48(v uint64) {
b.add(byte(v>>40), byte(v>>32), byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
// AddUint64 appends a big-endian, 64-bit value to the byte string.
func (b *Builder) AddUint64(v uint64) {
b.add(byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
// AddBytes appends a sequence of bytes to the byte string.
func (b *Builder) AddBytes(v []byte) {
b.add(v...)
}
// BuilderContinuation is a continuation-passing interface for building
// length-prefixed byte sequences. Builder methods for length-prefixed
// sequences (AddUint8LengthPrefixed etc) will invoke the BuilderContinuation
// supplied to them. The child builder passed to the continuation can be used
// to build the content of the length-prefixed sequence. For example:
//
// parent := cryptobyte.NewBuilder()
// parent.AddUint8LengthPrefixed(func (child *Builder) {
// child.AddUint8(42)
// child.AddUint8LengthPrefixed(func (grandchild *Builder) {
// grandchild.AddUint8(5)
// })
// })
//
// It is an error to write more bytes to the child than allowed by the reserved
// length prefix. After the continuation returns, the child must be considered
// invalid, i.e. users must not store any copies or references of the child
// that outlive the continuation.
//
// If the continuation panics with a value of type BuildError then the inner
// error will be returned as the error from Bytes. If the child panics
// otherwise then Bytes will repanic with the same value.
type BuilderContinuation func(child *Builder)
// BuildError wraps an error. If a BuilderContinuation panics with this value,
// the panic will be recovered and the inner error will be returned from
// Builder.Bytes.
type BuildError struct {
Err error
}
// AddUint8LengthPrefixed adds a 8-bit length-prefixed byte sequence.
func (b *Builder) AddUint8LengthPrefixed(f BuilderContinuation) {
b.addLengthPrefixed(1, false, f)
}
// AddUint16LengthPrefixed adds a big-endian, 16-bit length-prefixed byte sequence.
func (b *Builder) AddUint16LengthPrefixed(f BuilderContinuation) {
b.addLengthPrefixed(2, false, f)
}
// AddUint24LengthPrefixed adds a big-endian, 24-bit length-prefixed byte sequence.
func (b *Builder) AddUint24LengthPrefixed(f BuilderContinuation) {
b.addLengthPrefixed(3, false, f)
}
// AddUint32LengthPrefixed adds a big-endian, 32-bit length-prefixed byte sequence.
func (b *Builder) AddUint32LengthPrefixed(f BuilderContinuation) {
b.addLengthPrefixed(4, false, f)
}
func (b *Builder) callContinuation(f BuilderContinuation, arg *Builder) {
if !*b.inContinuation {
*b.inContinuation = true
defer func() {
*b.inContinuation = false
r := recover()
if r == nil {
return
}
if buildError, ok := r.(BuildError); ok {
b.err = buildError.Err
} else {
panic(r)
}
}()
}
f(arg)
}
func (b *Builder) addLengthPrefixed(lenLen int, isASN1 bool, f BuilderContinuation) {
// Subsequent writes can be ignored if the builder has encountered an error.
if b.err != nil {
return
}
offset := len(b.result)
b.add(make([]byte, lenLen)...)
if b.inContinuation == nil {
b.inContinuation = new(bool)
}
b.child = &Builder{
result: b.result,
fixedSize: b.fixedSize,
offset: offset,
pendingLenLen: lenLen,
pendingIsASN1: isASN1,
inContinuation: b.inContinuation,
}
b.callContinuation(f, b.child)
b.flushChild()
if b.child != nil {
panic("cryptobyte: internal error")
}
}
func (b *Builder) flushChild() {
if b.child == nil {
return
}
b.child.flushChild()
child := b.child
b.child = nil
if child.err != nil {
b.err = child.err
return
}
length := len(child.result) - child.pendingLenLen - child.offset
if length < 0 {
panic("cryptobyte: internal error") // result unexpectedly shrunk
}
if child.pendingIsASN1 {
// For ASN.1, we reserved a single byte for the length. If that turned out
// to be incorrect, we have to move the contents along in order to make
// space.
if child.pendingLenLen != 1 {
panic("cryptobyte: internal error")
}
var lenLen, lenByte uint8
if int64(length) > 0xfffffffe {
b.err = errors.New("pending ASN.1 child too long")
return
} else if length > 0xffffff {
lenLen = 5
lenByte = 0x80 | 4
} else if length > 0xffff {
lenLen = 4
lenByte = 0x80 | 3
} else if length > 0xff {
lenLen = 3
lenByte = 0x80 | 2
} else if length > 0x7f {
lenLen = 2
lenByte = 0x80 | 1
} else {
lenLen = 1
lenByte = uint8(length)
length = 0
}
// Insert the initial length byte, make space for successive length bytes,
// and adjust the offset.
child.result[child.offset] = lenByte
extraBytes := int(lenLen - 1)
if extraBytes != 0 {
child.add(make([]byte, extraBytes)...)
childStart := child.offset + child.pendingLenLen
copy(child.result[childStart+extraBytes:], child.result[childStart:])
}
child.offset++
child.pendingLenLen = extraBytes
}
l := length
for i := child.pendingLenLen - 1; i >= 0; i-- {
child.result[child.offset+i] = uint8(l)
l >>= 8
}
if l != 0 {
b.err = fmt.Errorf("cryptobyte: pending child length %d exceeds %d-byte length prefix", length, child.pendingLenLen)
return
}
if b.fixedSize && &b.result[0] != &child.result[0] {
panic("cryptobyte: BuilderContinuation reallocated a fixed-size buffer")
}
b.result = child.result
}
func (b *Builder) add(bytes ...byte) {
if b.err != nil {
return
}
if b.child != nil {
panic("cryptobyte: attempted write while child is pending")
}
if len(b.result)+len(bytes) < len(bytes) {
b.err = errors.New("cryptobyte: length overflow")
}
if b.fixedSize && len(b.result)+len(bytes) > cap(b.result) {
b.err = errors.New("cryptobyte: Builder is exceeding its fixed-size buffer")
return
}
b.result = append(b.result, bytes...)
}
// Unwrite rolls back non-negative n bytes written directly to the Builder.
// An attempt by a child builder passed to a continuation to unwrite bytes
// from its parent will panic.
func (b *Builder) Unwrite(n int) {
if b.err != nil {
return
}
if b.child != nil {
panic("cryptobyte: attempted unwrite while child is pending")
}
length := len(b.result) - b.pendingLenLen - b.offset
if length < 0 {
panic("cryptobyte: internal error")
}
if n < 0 {
panic("cryptobyte: attempted to unwrite negative number of bytes")
}
if n > length {
panic("cryptobyte: attempted to unwrite more than was written")
}
b.result = b.result[:len(b.result)-n]
}
// A MarshalingValue marshals itself into a Builder.
type MarshalingValue interface {
// Marshal is called by Builder.AddValue. It receives a pointer to a builder
// to marshal itself into. It may return an error that occurred during
// marshaling, such as unset or invalid values.
Marshal(b *Builder) error
}
// AddValue calls Marshal on v, passing a pointer to the builder to append to.
// If Marshal returns an error, it is set on the Builder so that subsequent
// appends don't have an effect.
func (b *Builder) AddValue(v MarshalingValue) {
err := v.Marshal(b)
if err != nil {
b.err = err
}
}
+183
View File
@@ -0,0 +1,183 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package cryptobyte contains types that help with parsing and constructing
// length-prefixed, binary messages, including ASN.1 DER. (The asn1 subpackage
// contains useful ASN.1 constants.)
//
// The String type is for parsing. It wraps a []byte slice and provides helper
// functions for consuming structures, value by value.
//
// The Builder type is for constructing messages. It providers helper functions
// for appending values and also for appending length-prefixed submessages
// without having to worry about calculating the length prefix ahead of time.
//
// See the documentation and examples for the Builder and String types to get
// started.
package cryptobyte
// String represents a string of bytes. It provides methods for parsing
// fixed-length and length-prefixed values from it.
type String []byte
// read advances a String by n bytes and returns them. If less than n bytes
// remain, it returns nil.
func (s *String) read(n int) []byte {
if len(*s) < n || n < 0 {
return nil
}
v := (*s)[:n]
*s = (*s)[n:]
return v
}
// Skip advances the String by n byte and reports whether it was successful.
func (s *String) Skip(n int) bool {
return s.read(n) != nil
}
// ReadUint8 decodes an 8-bit value into out and advances over it.
// It reports whether the read was successful.
func (s *String) ReadUint8(out *uint8) bool {
v := s.read(1)
if v == nil {
return false
}
*out = uint8(v[0])
return true
}
// ReadUint16 decodes a big-endian, 16-bit value into out and advances over it.
// It reports whether the read was successful.
func (s *String) ReadUint16(out *uint16) bool {
v := s.read(2)
if v == nil {
return false
}
*out = uint16(v[0])<<8 | uint16(v[1])
return true
}
// ReadUint24 decodes a big-endian, 24-bit value into out and advances over it.
// It reports whether the read was successful.
func (s *String) ReadUint24(out *uint32) bool {
v := s.read(3)
if v == nil {
return false
}
*out = uint32(v[0])<<16 | uint32(v[1])<<8 | uint32(v[2])
return true
}
// ReadUint32 decodes a big-endian, 32-bit value into out and advances over it.
// It reports whether the read was successful.
func (s *String) ReadUint32(out *uint32) bool {
v := s.read(4)
if v == nil {
return false
}
*out = uint32(v[0])<<24 | uint32(v[1])<<16 | uint32(v[2])<<8 | uint32(v[3])
return true
}
// ReadUint48 decodes a big-endian, 48-bit value into out and advances over it.
// It reports whether the read was successful.
func (s *String) ReadUint48(out *uint64) bool {
v := s.read(6)
if v == nil {
return false
}
*out = uint64(v[0])<<40 | uint64(v[1])<<32 | uint64(v[2])<<24 | uint64(v[3])<<16 | uint64(v[4])<<8 | uint64(v[5])
return true
}
// ReadUint64 decodes a big-endian, 64-bit value into out and advances over it.
// It reports whether the read was successful.
func (s *String) ReadUint64(out *uint64) bool {
v := s.read(8)
if v == nil {
return false
}
*out = uint64(v[0])<<56 | uint64(v[1])<<48 | uint64(v[2])<<40 | uint64(v[3])<<32 | uint64(v[4])<<24 | uint64(v[5])<<16 | uint64(v[6])<<8 | uint64(v[7])
return true
}
func (s *String) readUnsigned(out *uint32, length int) bool {
v := s.read(length)
if v == nil {
return false
}
var result uint32
for i := 0; i < length; i++ {
result <<= 8
result |= uint32(v[i])
}
*out = result
return true
}
func (s *String) readLengthPrefixed(lenLen int, outChild *String) bool {
lenBytes := s.read(lenLen)
if lenBytes == nil {
return false
}
var length uint32
for _, b := range lenBytes {
length = length << 8
length = length | uint32(b)
}
v := s.read(int(length))
if v == nil {
return false
}
*outChild = v
return true
}
// ReadUint8LengthPrefixed reads the content of an 8-bit length-prefixed value
// into out and advances over it. It reports whether the read was successful.
func (s *String) ReadUint8LengthPrefixed(out *String) bool {
return s.readLengthPrefixed(1, out)
}
// ReadUint16LengthPrefixed reads the content of a big-endian, 16-bit
// length-prefixed value into out and advances over it. It reports whether the
// read was successful.
func (s *String) ReadUint16LengthPrefixed(out *String) bool {
return s.readLengthPrefixed(2, out)
}
// ReadUint24LengthPrefixed reads the content of a big-endian, 24-bit
// length-prefixed value into out and advances over it. It reports whether
// the read was successful.
func (s *String) ReadUint24LengthPrefixed(out *String) bool {
return s.readLengthPrefixed(3, out)
}
// ReadBytes reads n bytes into out and advances over them. It reports
// whether the read was successful.
func (s *String) ReadBytes(out *[]byte, n int) bool {
v := s.read(n)
if v == nil {
return false
}
*out = v
return true
}
// CopyBytes copies len(out) bytes into out and advances over them. It reports
// whether the copy operation was successful
func (s *String) CopyBytes(out []byte) bool {
n := len(out)
v := s.read(n)
if v == nil {
return false
}
return copy(out, v) == n
}
// Empty reports whether the string does not contain any bytes.
func (s String) Empty() bool {
return len(s) == 0
}
+17 -1
View File
@@ -348,6 +348,9 @@ func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey)
if cert.CertType != HostCert {
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
}
if c.IsHostAuthority == nil {
return errors.New("ssh: cannot verify certificate, IsHostAuthority not set")
}
if !c.IsHostAuthority(cert.SignatureKey, addr) {
return fmt.Errorf("ssh: no authorities for hostname: %v", addr)
}
@@ -375,6 +378,9 @@ func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permis
if cert.CertType != UserCert {
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
}
if c.IsUserAuthority == nil {
return nil, errors.New("ssh: cannot verify certificate, IsUserAuthority not set")
}
if !c.IsUserAuthority(cert.SignatureKey) {
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
@@ -438,7 +444,17 @@ func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
return fmt.Errorf("ssh: cert has expired")
}
if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
// Match OpenSSH: the SK user-presence flag is never enforced on a
// certificate's CA signature. OpenSSH calls sshkey_verify with
// detailsp==NULL in sshkey.c:cert_parse, so the UP/UV flags are
// not even extracted. The UP bit on a CA signature reflects the
// CA operator's presence at signing time, which has no bearing on
// whether the user being authenticated is present now; enforcing
// it here would only break interop with certificates issued by
// non-interactive SK CAs. skKeyWithoutUP is a no-op for non-SK
// keys (the common case).
caKey := skKeyWithoutUP(cert.SignatureKey)
if err := caKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
return fmt.Errorf("ssh: certificate signature does not verify")
}
+62 -6
View File
@@ -11,6 +11,7 @@ import (
"io"
"log"
"sync"
"sync/atomic"
)
const (
@@ -131,11 +132,17 @@ func (r RejectionReason) String() string {
return fmt.Sprintf("unknown reason %d", int(r))
}
func min(a uint32, b int) uint32 {
if a < uint32(b) {
return a
// minPayloadSize returns min(limit, length) clamped to a uint32. It is used
// to compute the size of the next channel data packet from the remaining
// payload. The comparison is done in int64 because length is an int — on
// 64-bit systems len(data) can exceed 2^32, and a direct uint32(length)
// cast would silently truncate to 0 at every multiple of 2^32, causing
// WriteExtended's loop to spin without making progress.
func minPayloadSize(limit uint32, length int) uint32 {
if int64(length) > int64(limit) {
return limit
}
return uint32(b)
return uint32(length)
}
type channelDirection uint8
@@ -177,6 +184,12 @@ type channel struct {
// with WantReply=true outstanding. This lock is held by a
// goroutine that has such an outgoing request pending.
sentRequestMu sync.Mutex
// sentRequestPending is set to true while a SendRequest call with
// WantReply=true is in flight. handlePacket uses it as a gate: responses
// arriving while no request is pending are dropped to prevent a
// misbehaving peer from stalling the mux read loop by filling ch.msg
// with unsolicited channelRequestSuccess/Failure messages.
sentRequestPending atomic.Bool
incomingRequests chan *Request
@@ -251,7 +264,7 @@ func (ch *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err e
ch.writeMu.Unlock()
for len(data) > 0 {
space := min(ch.maxRemotePayload, len(data))
space := minPayloadSize(ch.maxRemotePayload, len(data))
if space, err = ch.remoteWin.reserve(space); err != nil {
return n, err
}
@@ -460,6 +473,18 @@ func (ch *channel) handlePacket(packet []byte) error {
}
ch.incomingRequests <- &req
case *channelRequestSuccessMsg, *channelRequestFailureMsg:
// Drop responses that arrive when no SendRequest is waiting, to
// prevent a malicious peer from filling ch.msg and stalling the
// mux read loop. The non-blocking send additionally protects the
// loop if a well-behaved caller is slow to read.
if !ch.sentRequestPending.Load() {
return nil
}
select {
case ch.msg <- msg:
default:
}
default:
ch.msg <- msg
}
@@ -530,7 +555,17 @@ func (ch *channel) Reject(reason RejectionReason, message string) error {
Language: "en",
}
ch.decided = true
return ch.sendMessage(reject)
err := ch.sendMessage(reject)
// Remove the channel from the mux to prevent memory leaks.
// Do not call ch.close() here: no goroutine holds a reference to a
// rejected channel's internal channels (msg, incomingRequests), so
// removing it from chanList is sufficient for GC. Calling close()
// would race with the mux loop goroutine (handlePacket or dropAll),
// causing a panic from closing an already-closed channel.
ch.mux.chanList.remove(ch.localId)
return err
}
func (ch *channel) Read(data []byte) (int, error) {
@@ -586,6 +621,27 @@ func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo
if wantReply {
ch.sentRequestMu.Lock()
defer ch.sentRequestMu.Unlock()
// Open the gate so that responses arriving while this request is in
// flight are allowed to reach ch.msg. Responses arriving while no
// request is pending are dropped by handlePacket.
ch.sentRequestPending.Store(true)
defer ch.sentRequestPending.Store(false)
// Drain any spurious responses that may have been buffered. This
// prevents a previously buffered unexpected response from being
// consumed instead of the actual response for this request.
drain:
for {
select {
case _, ok := <-ch.msg:
if !ok {
break drain
}
default:
break drain
}
}
}
msg := channelRequestMsg{
+1 -1
View File
@@ -407,7 +407,7 @@ func (c *gcmCipher) readCipherPacket(seqNum uint32, r io.Reader) ([]byte, error)
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
}
if int(padding+1) >= len(plain) {
if int(padding)+1 >= len(plain) {
return nil, fmt.Errorf("ssh: padding %d too large", padding)
}
plain = plain[1 : length-uint32(padding)]
+85
View File
@@ -88,6 +88,32 @@ func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
}
// NewControlClientConn establishes an SSH connection over an OpenSSH
// ControlMaster socket c in proxy mode.
//
// Note that this package only implements the client side of the multiplexing
// protocol. The provided net.Conn must be a local, secure connection (such as a
// Unix domain socket) connected to an already-running OpenSSH process acting as
// the ControlMaster.
//
// WARNING: Because proxy mode bypasses the standard cryptographic handshake
// passing a standard network connection (e.g., TCP) will result in plaintext
// data leakage.
//
// The Request and NewChannel channels must be serviced or the connection
// will hang.
func NewControlClientConn(c net.Conn) (Conn, <-chan NewChannel, <-chan *Request, error) {
conn := &connection{
sshConn: sshConn{conn: c},
}
var err error
if conn.transport, err = handshakeControlProxy(c); err != nil {
return nil, nil, nil, fmt.Errorf("ssh: control proxy handshake failed: %w", err)
}
conn.mux = newMux(conn.transport)
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
}
// clientHandshake performs the client side key exchange. See RFC 4253 Section
// 7.
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
@@ -197,6 +223,59 @@ type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
// the server. A BannerCallback receives the message sent by the remote server.
type BannerCallback func(message string) error
// ClientAuthContext contains information about the current state of the
// authentication process, passed to [ClientAuthCallback].
type ClientAuthContext struct {
// Metadata contains the connection metadata.
Metadata ConnMetadata
// Algorithms contains the negotiated algorithms.
Algorithms NegotiatedAlgorithms
// AllowedMethods lists the authentication methods currently accepted
// by the server. These are the protocol-level names defined in RFC 4252
// such as "publickey", "password".
AllowedMethods []string
// PartialSuccessMethods lists the authentication methods that have already
// succeeded, indicating a multi-step authentication flow. This list
// represents the exact sequence of partial successes and may contain
// duplicates if the same method succeeded multiple times.
PartialSuccessMethods []string
// TriedMethods lists the methods that have already been attempted and
// failed during this session. This list represents the exact sequence of
// failures and may contain duplicates. This allows the callback to also
// track the number of failed attempts for a specific method.
TriedMethods []string
}
// ClientAuthCallback is a hook invoked before each authentication attempt. It
// allows the client to dynamically select an authentication method based on the
// current context, server capabilities, or previous failures.
//
// The callback is invoked after the initial "none" authentication method, once
// the server's supported authentication methods are known.
//
// Return values:
// - (AuthMethod, nil): The client will attempt this specific method next.
// The returned method does NOT need to be present in [ClientConfig.Auth].
// This allows for dynamic authentication strategies (e.g., prompting
// for a password only if public key auth fails). Callers should inspect
// [ClientAuthContext.TriedMethods] to avoid repeatedly returning the
// same failing method.
// - (nil, nil): The client selects from [ClientConfig.Auth] the first
// instance of a method that has not been tried yet, or aborts if none
// are left. If authentication is not successful, the callback is invoked
// again before the following attempt.
// - (nil, error): The authentication process is aborted immediately,
// causing the ongoing SSH handshake to fail with the provided error.
//
// To bound resource use, the client caps the total number of authentication
// attempts (failures and partial successes combined) at 64. If the cap is
// exceeded the handshake aborts with an error.
type ClientAuthCallback func(ctx *ClientAuthContext) (AuthMethod, error)
// A ClientConfig structure is used to configure a Client. It must not be
// modified after having been passed to an SSH function.
type ClientConfig struct {
@@ -210,6 +289,9 @@ type ClientConfig struct {
// Auth contains possible authentication methods to use with the
// server. Only the first instance of a particular RFC 4252 method will
// be used during authentication.
//
// If AuthCallback is set, these AuthMethod are only used if the
// callback returns nil.
Auth []AuthMethod
// HostKeyCallback is called during the cryptographic
@@ -240,6 +322,9 @@ type ClientConfig struct {
//
// A Timeout of zero means no timeout.
Timeout time.Duration
// AuthCallback, if non-nil, is invoked before each authentication attempt.
AuthCallback ClientAuthCallback
}
// InsecureIgnoreHostKey returns a function that can be used for
+50 -14
View File
@@ -21,6 +21,12 @@ const (
authSuccess
)
// maxAuthClientTried bounds the total number of authentication attempts
// (failures and partial successes combined) the client makes before
// aborting the loop, to prevent unbounded growth when an AuthCallback
// keeps supplying methods.
const maxAuthClientTried = 64
// clientAuthenticate authenticates with the remote server. See RFC 4252.
func (c *connection) clientAuthenticate(config *ClientConfig) error {
// initiate user auth session
@@ -67,32 +73,62 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
// then any untried methods suggested by the server.
var tried []string
var lastMethods []string
var partialSuccess []string
sessionID := c.transport.getSessionID()
for auth := AuthMethod(new(noneAuth)); auth != nil; {
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
if err != nil {
// On disconnect, return error immediately
if _, ok := err.(*disconnectMsg); ok {
if _, isDisconnect := err.(*disconnectMsg); isDisconnect {
return err
}
// We return the error later if there is no other method left to
// try.
// We return the error later if there is no other method
// left to try.
ok = authFailure
}
if ok == authSuccess {
// success
switch ok {
case authSuccess:
return nil
} else if ok == authFailure {
if m := auth.method(); !slices.Contains(tried, m) {
tried = append(tried, m)
}
case authPartialSuccess:
partialSuccess = append(partialSuccess, auth.method())
case authFailure:
tried = append(tried, auth.method())
}
if len(partialSuccess)+len(tried) > maxAuthClientTried {
return fmt.Errorf("ssh: too many authentication attempts (%d), aborting",
len(partialSuccess)+len(tried))
}
if methods == nil {
methods = lastMethods
}
lastMethods = methods
// If AuthCallback is set it takes precedence: it picks the next
// AuthMethod dynamically. The returned method need not be in
// config.Auth. If the callback returns (nil, nil) we fall back to
// selecting the next untried method from config.Auth below; on
// (nil, error) the handshake aborts.
if config.AuthCallback != nil {
ctx := &ClientAuthContext{
Metadata: c,
Algorithms: c.Algorithms(),
AllowedMethods: slices.Clone(methods),
PartialSuccessMethods: slices.Clone(partialSuccess),
TriedMethods: slices.Clone(tried),
}
altAuth, cbErr := config.AuthCallback(ctx)
if cbErr != nil {
return cbErr
}
if altAuth != nil {
auth = altAuth
continue
}
}
auth = nil
findNext:
@@ -377,11 +413,11 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
return authFailure, nil, err
}
// If authentication succeeds or the list of available methods does not
// contain the "publickey" method, do not attempt to authenticate with any
// other keys. According to RFC 4252 Section 7, the latter can occur when
// additional authentication methods are required.
if success == authSuccess || !slices.Contains(methods, cb.method()) {
// If authentication succeeds or partially succeeds, return immediately
// so the caller can select the next auth method. According to RFC 4252
// Section 7, if the server no longer lists "publickey" among its
// allowed methods, do not attempt to authenticate with any other keys.
if success == authSuccess || success == authPartialSuccess || !slices.Contains(methods, cb.method()) {
return success, methods, err
}
}
+9 -1
View File
@@ -91,9 +91,17 @@ func DiscardRequests(in <-chan *Request) {
}
}
// A connTransport represents the transport for a connection.
type connTransport interface {
packetConn
getAlgorithms() NegotiatedAlgorithms
getSessionID() []byte
waitSession() error
}
// A connection represents an incoming connection.
type connection struct {
transport *handshakeTransport
transport connTransport
sshConn
// The connection protocol.
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"encoding/binary"
"errors"
"fmt"
"io"
"golang.org/x/crypto/cryptobyte"
)
const (
muxProtocolVersion = 4
muxMsgHello = 0x00000001
muxCProxy = 0x1000000f
muxSProxy = 0x8000000f
)
const controlProxyRequestID = 0
// handshakeControlProxy attempts to establish a transport connection with an
// OpenSSH ControlMaster socket in proxy mode. For details see:
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux
func handshakeControlProxy(rw io.ReadWriteCloser) (connTransport, error) {
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
b.AddUint32(muxMsgHello)
b.AddUint32(muxProtocolVersion)
}); err != nil {
return nil, fmt.Errorf("mux hello write failed: %w", err)
}
if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) {
b.AddUint32(muxCProxy)
b.AddUint32(controlProxyRequestID)
}); err != nil {
return nil, fmt.Errorf("mux client proxy write failed: %w", err)
}
messageType, body, err := controlProxyReadMessage(rw)
if err != nil {
return nil, fmt.Errorf("mux hello read failed: %w", err)
}
if messageType != muxMsgHello {
return nil, fmt.Errorf("expected hello response, got %v", messageType)
}
var v uint32
if !body.ReadUint32(&v) {
return nil, errors.New("EOF reading mux protocol version")
}
if v != muxProtocolVersion {
return nil, fmt.Errorf("mux server has unsupported version %v", v)
}
messageType, body, err = controlProxyReadMessage(rw)
if err != nil {
return nil, fmt.Errorf("mux server proxy read failed: %w", err)
}
if messageType != muxSProxy {
return nil, fmt.Errorf("expected server proxy response, got %v", messageType)
}
var reqID uint32
if !body.ReadUint32(&reqID) {
return nil, errors.New("EOF reading request id")
}
if reqID != controlProxyRequestID {
return nil, fmt.Errorf("expected request id %v, got %v", controlProxyRequestID, reqID)
}
return &controlProxyTransport{rw}, nil
}
// controlProxyTransport implements the connTransport interface for
// ControlMaster connections. Each controlMessage has zero length padding and
// no MAC.
type controlProxyTransport struct {
rw io.ReadWriteCloser
}
func (p *controlProxyTransport) Close() error {
return p.rw.Close()
}
func (p *controlProxyTransport) writePacket(controlMessage []byte) error {
return controlProxyWritePacket(p.rw, func(b *cryptobyte.Builder) {
b.AddUint8(0) // Padding length.
b.AddBytes(controlMessage)
})
}
func (p *controlProxyTransport) readPacket() ([]byte, error) {
buf, err := controlProxyReadPacket(p.rw)
if err != nil {
return nil, fmt.Errorf("ssh: error reading control message: %w", err)
}
// Discard the padding length.
if len(buf) < 1 {
return nil, errors.New("ssh: EOF reading padding length")
}
if buf[0] != 0 {
return nil, errors.New("ssh: unexpected non-zero padding in control message")
}
return buf[1:], nil
}
func (p *controlProxyTransport) getAlgorithms() NegotiatedAlgorithms {
return NegotiatedAlgorithms{}
}
func (p *controlProxyTransport) getSessionID() []byte {
return nil
}
func (p *controlProxyTransport) waitSession() error {
return nil
}
func controlProxyWritePacket(w io.Writer, f cryptobyte.BuilderContinuation) error {
var buf []byte
b := cryptobyte.NewBuilder(buf)
b.AddUint32LengthPrefixed(f)
out, err := b.Bytes()
if err != nil {
return err
}
_, err = w.Write(out)
return err
}
func controlProxyReadPacket(r io.Reader) (cryptobyte.String, error) {
var l uint32
if err := binary.Read(r, binary.BigEndian, &l); err != nil {
return nil, err
}
if l > maxPacket {
return nil, fmt.Errorf("message length %v exceeds maximum %v", l, maxPacket)
}
buf := make([]byte, l)
if _, err := io.ReadFull(r, buf); err != nil {
return nil, err
}
return buf, nil
}
func controlProxyReadMessage(r io.Reader) (messageType uint32, body cryptobyte.String, err error) {
body, err = controlProxyReadPacket(r)
if err != nil {
return 0, nil, fmt.Errorf("error reading message body: %w", err)
}
if !body.ReadUint32(&messageType) {
return 0, nil, errors.New("EOF reading message type")
}
return messageType, body, nil
}
+66 -9
View File
@@ -16,6 +16,7 @@ import (
"io"
"math/big"
"slices"
"sync"
"golang.org/x/crypto/curve25519"
)
@@ -718,15 +719,9 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
kexDHGexRequest.MaxBits, kexDHGexRequest.PreferredBits)
}
var p *big.Int
// We hardcode sending Oakley Group 14 (2048 bits), Oakley Group 15 (3072
// bits) or Oakley Group 16 (4096 bits), based on the requested max size.
if kexDHGexRequest.MaxBits < 3072 {
p, _ = new(big.Int).SetString(oakleyGroup14, 16)
} else if kexDHGexRequest.MaxBits < 4096 {
p, _ = new(big.Int).SetString(oakleyGroup15, 16)
} else {
p, _ = new(big.Int).SetString(oakleyGroup16, 16)
p, err := chooseDH(kexDHGexRequest)
if err != nil {
return nil, err
}
g := big.NewInt(2)
@@ -805,3 +800,65 @@ func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshak
Hash: gex.hashFunc,
}, err
}
type dhKEXGroup struct {
size int
p *big.Int
}
// supportedDHKEXGroups returns the DH groups the server is willing to offer
// for diffie-hellman-group-exchange-* key exchanges. The list is built lazily
// on first use to keep the hex-to-big.Int parse out of package initialization.
var supportedDHKEXGroups = sync.OnceValue(func() []dhKEXGroup {
specs := []struct {
size int
hex string
}{
{2048, oakleyGroup14},
{3072, oakleyGroup15},
{4096, oakleyGroup16},
}
out := make([]dhKEXGroup, 0, len(specs))
for _, s := range specs {
p, _ := new(big.Int).SetString(s.hex, 16)
out = append(out, dhKEXGroup{size: s.size, p: p})
}
return out
})
// chooseDH picks a DH group for the given client request, mirroring the
// algorithm used by OpenSSH's choose_dh in dh.c: prefer the smallest known
// group larger than or equal to the client's PreferredBits, and otherwise pick
// the largest group within the accepted [MinBits, MaxBits] range.
func chooseDH(req kexDHGexRequestMsg) (*big.Int, error) {
var best *big.Int
bestSize := 0
wantBits := int(req.PreferredBits)
for _, group := range supportedDHKEXGroups() {
if uint32(group.size) < req.MinBits || uint32(group.size) > req.MaxBits {
continue
}
if bestSize == 0 {
best = group.p
bestSize = group.size
continue
}
closerFromAbove := group.size >= wantBits && group.size < bestSize
closerFromBelow := group.size > bestSize && bestSize < wantBits
if closerFromAbove || closerFromBelow {
best = group.p
bestSize = group.size
}
}
if bestSize == 0 {
return nil, fmt.Errorf("ssh: no suitable DH group found for request min: %d, preferred: %d, max: %d",
req.MinBits, req.PreferredBits, req.MaxBits)
}
return best, nil
}
+96 -3
View File
@@ -76,7 +76,7 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err
case InsecureKeyAlgoDSA:
return parseDSA(in)
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
return parseECDSA(in)
return parseECDSA(in, algo)
case KeyAlgoSKECDSA256:
return parseSKECDSA(in)
case KeyAlgoED25519:
@@ -469,6 +469,12 @@ func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
// 8192 bits is also the maximum RSA key size accepted by crypto/tls for
// signature verification:
// https://github.com/golang/go/blob/69801b25/src/crypto/tls/handshake_client.go#L1096
if w.N.BitLen() > 8192 {
return nil, nil, errors.New("ssh: rsa modulus too large")
}
if w.E.BitLen() > 24 {
return nil, nil, errors.New("ssh: exponent too large")
}
@@ -574,6 +580,24 @@ func checkDSAParams(param *dsa.Parameters) error {
return fmt.Errorf("ssh: unsupported DSA key size %d", l)
}
// FIPS 186-2 specifies that Q must be exactly 160 bits. We must enforce
// this to prevent DoS attacks where an attacker sends a huge Q which makes
// verification slow.
if l := param.Q.BitLen(); l != 160 {
return fmt.Errorf("ssh: unsupported DSA sub-prime size %d", l)
}
// The generator G is an element of the group, so it must be strictly less
// than the modulus P.
if param.G.Cmp(param.P) >= 0 {
return errors.New("ssh: DSA generator larger than modulus")
}
// G must be positive.
if param.G.Sign() <= 0 {
return errors.New("ssh: DSA generator must be positive")
}
return nil
}
@@ -596,6 +620,14 @@ func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
// The public value Y must be a non-zero element of the group, i.e.
// strictly between 0 and P. crypto/dsa.Verify does not range-check Y,
// so we reject out-of-range values here to prevent a maliciously
// oversized Y from slowing verification.
if w.Y.Sign() <= 0 || w.Y.Cmp(w.P) >= 0 {
return nil, nil, errors.New("ssh: DSA public value Y out of range")
}
key := &dsaPublicKey{
Parameters: param,
Y: w.Y,
@@ -774,7 +806,7 @@ func supportedEllipticCurve(curve elliptic.Curve) bool {
}
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
func parseECDSA(in []byte, expectedType string) (out PublicKey, rest []byte, err error) {
var w struct {
Curve string
KeyBytes []byte
@@ -785,6 +817,12 @@ func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
return nil, nil, err
}
actualType := "ecdsa-sha2-" + w.Curve
if expectedType != actualType {
return nil, nil, fmt.Errorf("ssh: algorithm type mismatch: expected %q, found curve %q (type %q)",
expectedType, w.Curve, actualType)
}
key := new(ecdsa.PublicKey)
switch w.Curve {
@@ -869,11 +907,25 @@ type skFields struct {
Counter uint32
}
// flagUserPresence is the "user present" bit (UP) in the SK signature
// flags, matching the FIDO CTAP2 authenticatorData UP flag. See
// openssh/PROTOCOL.u2f.
const flagUserPresence = 0x01
// errSKMissingUserPresence is returned by SK key Verify methods when
// the signature does not assert user presence and the key was not
// marked as no-touch-required.
var errSKMissingUserPresence = errors.New("ssh: signature missing required user presence flag")
type skECDSAPublicKey struct {
// application is a URL-like string, typically "ssh:" for SSH.
// see openssh/PROTOCOL.u2f for details.
application string
ecdsa.PublicKey
// noTouchRequired, when true, disables the default user-presence
// check in Verify. It is set by skKeyWithoutUP on a clone of the
// key, never on an instance shared across authentication attempts.
noTouchRequired bool
}
func (k *skECDSAPublicKey) Type() string {
@@ -959,6 +1011,10 @@ func (k *skECDSAPublicKey) Verify(data []byte, sig *Signature) error {
return err
}
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
return errSKMissingUserPresence
}
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
@@ -992,6 +1048,10 @@ type skEd25519PublicKey struct {
// see openssh/PROTOCOL.u2f for details.
application string
ed25519.PublicKey
// noTouchRequired, when true, disables the default user-presence
// check in Verify. It is set by skKeyWithoutUP on a clone of the
// key, never on an instance shared across authentication attempts.
noTouchRequired bool
}
func (k *skEd25519PublicKey) Type() string {
@@ -1066,6 +1126,10 @@ func (k *skEd25519PublicKey) Verify(data []byte, sig *Signature) error {
return err
}
if skf.Flags&flagUserPresence == 0 && !k.noTouchRequired {
return errSKMissingUserPresence
}
blob := struct {
ApplicationDigest []byte `ssh:"rest"`
Flags byte
@@ -1408,6 +1472,17 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc {
return nil, err
}
// OpenSSH does not impose an upper bound on the bcrypt round count
// stored in the key file, but bcrypt_pbkdf cost is linear in rounds:
// the default is 16, ssh-keygen lets users pick anything up to
// INT_MAX. Cap at 2048 (128x the default, a few seconds of CPU) so
// that an oversized value in the file cannot tie up the caller for
// months.
const maxRounds = 1 << 11
if opts.Rounds > maxRounds {
return nil, fmt.Errorf("ssh: bcrypt KDF rounds %d exceed maximum %d", opts.Rounds, maxRounds)
}
k, err := bcrypt_pbkdf.Key(passphrase, []byte(opts.Salt), int(opts.Rounds), 32+16)
if err != nil {
return nil, err
@@ -1577,10 +1652,28 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
return nil, err
}
// Mirror the validation done in parseRSA for public keys: cap the
// modulus at the same limit enforced by crypto/tls, reject oversized
// or invalid exponents, and additionally bound the prime factors to
// avoid the expensive CRT coefficient recomputation in pk.Precompute.
if key.N.BitLen() > 8192 {
return nil, errors.New("ssh: rsa modulus too large")
}
if key.P.BitLen() > 4096 || key.Q.BitLen() > 4096 {
return nil, errors.New("ssh: rsa prime too large")
}
if key.E.BitLen() > 24 {
return nil, errors.New("ssh: exponent too large")
}
e := key.E.Int64()
if e < 3 || e&1 == 0 {
return nil, errors.New("ssh: incorrect exponent")
}
pk := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
N: key.N,
E: int(key.E.Int64()),
E: int(e),
},
D: key.D,
Primes: []*big.Int{key.P, key.Q},
+35 -4
View File
@@ -91,9 +91,10 @@ type mux struct {
incomingChannels chan NewChannel
globalSentMu sync.Mutex
globalResponses chan interface{}
incomingRequests chan *Request
globalSentMu sync.Mutex
globalSentPending atomic.Bool
globalResponses chan interface{}
incomingRequests chan *Request
errCond *sync.Cond
err error
@@ -141,6 +142,27 @@ func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []
if wantReply {
m.globalSentMu.Lock()
defer m.globalSentMu.Unlock()
// Open the gate so that responses arriving while this request is in
// flight are allowed to reach globalResponses. Any response arriving
// while no request is pending is dropped by handleGlobalPacket.
m.globalSentPending.Store(true)
defer m.globalSentPending.Store(false)
// Drain any spurious responses that may have been buffered. This prevents
// a previously buffered unexpected response from being consumed instead
// of the actual response for this request.
drain:
for {
select {
case _, ok := <-m.globalResponses:
if !ok {
break drain
}
default:
break drain
}
}
}
if err := m.sendMessage(globalRequestMsg{
@@ -267,7 +289,16 @@ func (m *mux) handleGlobalPacket(packet []byte) error {
mux: m,
}
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
m.globalResponses <- msg
// Drop responses that arrive when no SendRequest is waiting, to
// prevent a malicious peer from staging responses for a future
// caller.
if !m.globalSentPending.Load() {
return nil
}
select {
case m.globalResponses <- msg:
default:
}
default:
panic(fmt.Sprintf("not a global message %#v", msg))
}
+147 -18
View File
@@ -34,21 +34,29 @@ type Permissions struct {
// or not supported.
CriticalOptions map[string]string
// Extensions are extra functionality that the server may
// offer on authenticated connections. Lack of support for an
// extension does not preclude authenticating a user. Common
// extensions are "permit-agent-forwarding",
// "permit-X11-forwarding". The Go SSH library currently does
// not act on any extension, and it is up to server
// implementations to honor them. Extensions can be used to
// pass data from the authentication callbacks to the server
// application layer.
// Extensions are extra functionality that the server may offer on
// authenticated connections. Lack of support for an extension does not
// preclude authenticating a user. Common extensions are
// "permit-agent-forwarding", "permit-X11-forwarding". In general the Go
// SSH library does not act on extensions and it is up to server
// implementations to honor them; extensions can also be used to pass data
// from the authentication callbacks to the server application layer.
//
// The one extension acted upon by this library is "no-touch-required",
// which applies only to security-key public keys
// (sk-ecdsa-sha2-nistp256@openssh.com and sk-ssh-ed25519@openssh.com).
// When present, it waives the default requirement that SK signatures
// assert user presence (i.e. a physical touch of the authenticator)
// during signature verification.
Extensions map[string]string
// ExtraData allows to store user defined data.
ExtraData map[any]any
}
// GSSAPIWithMICConfig includes the server callbacks for gssapi-with-mic
// authentication. If either field is nil, gssapi-with-mic is considered not
// configured.
type GSSAPIWithMICConfig struct {
// AllowLogin, must be set, is called when gssapi-with-mic
// authentication is selected (RFC 4462 section 3). The srcName is from the
@@ -63,6 +71,10 @@ type GSSAPIWithMICConfig struct {
Server GSSAPIServer
}
func gssapiWithMICConfigured(config *GSSAPIWithMICConfig) bool {
return config != nil && config.AllowLogin != nil && config.Server != nil
}
// SendAuthBanner implements [ServerPreAuthConn].
func (s *connection) SendAuthBanner(msg string) error {
return s.transport.writePacket(Marshal(&userAuthBannerMsg{
@@ -84,6 +96,79 @@ type ServerPreAuthConn interface {
SendAuthBanner(string) error
}
// noTouchRequiredExtension is the extension name used by OpenSSH in
// authorized_keys options and certificate extensions to mark keys
// whose signatures do not need to assert user presence (touch). See
// ssh-keygen(1) and sshd(8).
const noTouchRequiredExtension = "no-touch-required"
// noTouchAllowed reports whether the user presence requirement on
// SK signatures should be waived for this authentication attempt. The
// requirement is waived when the "no-touch-required" extension is
// present either in the Permissions returned by the auth callback
// (authorized_keys-level opt-out) or in the certificate's own
// Extensions (CA-level opt-out), matching OpenSSH behavior. OpenSSH
// reads the per-key opt-out only from cert Extensions and
// authorized_keys options (never from CriticalOptions); we follow the
// same rule.
func noTouchAllowed(pubKey PublicKey, perms *Permissions) bool {
if perms != nil {
if _, ok := perms.Extensions[noTouchRequiredExtension]; ok {
return true
}
}
if cert, ok := pubKey.(*Certificate); ok {
if _, ok := cert.Extensions[noTouchRequiredExtension]; ok {
return true
}
}
return false
}
// skKeyWithoutUP returns a PublicKey equivalent to pubKey but whose
// Verify accepts SK signatures with the user-presence flag clear. If
// pubKey is not (and does not wrap) an SK key, pubKey is returned
// unchanged. The returned value never mutates pubKey: for SK keys a
// shallow copy is made so that the noTouchRequired flag is set only on
// the clone.
//
// The implementation is iterative rather than recursive. When pubKey
// is a *Certificate we unwrap exactly one level to look at the inner
// key. The SSH cert format forbids Certificate.Key from being another
// Certificate (parseCert rejects it), but nothing stops callers from
// constructing such a value directly in Go; a recursive descent could
// otherwise be driven to unbounded depth by a hand-crafted or cyclic
// Certificate. A malformed input of that shape simply returns
// unchanged here.
func skKeyWithoutUP(pubKey PublicKey) PublicKey {
cert, isCert := pubKey.(*Certificate)
target := pubKey
if isCert {
target = cert.Key
}
var cloned PublicKey
switch k := target.(type) {
case *skECDSAPublicKey:
c := *k
c.noTouchRequired = true
cloned = &c
case *skEd25519PublicKey:
c := *k
c.noTouchRequired = true
cloned = &c
default:
// Not an SK key (or a pathological *Certificate wrapping
// another *Certificate): pubKey is already usable for Verify.
return pubKey
}
if !isCert {
return cloned
}
c := *cert
c.Key = cloned
return &c
}
// ServerConfig holds server specific configuration data.
type ServerConfig struct {
// Config contains configuration shared between client and server.
@@ -242,8 +327,10 @@ func (c *pubKeyCache) add(candidate cachedPubKey) {
type ServerConn struct {
Conn
// If the succeeding authentication callback returned a
// non-nil Permissions pointer, it is stored here.
// If the succeeding authentication callback returned a non-nil Permissions
// pointer, it is stored here. These are the permissions from the final,
// successful authentication method. Permissions returned by callbacks that
// return PartialSuccessError are not preserved and must be nil.
Permissions *Permissions
}
@@ -302,8 +389,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error)
}
if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil &&
config.KeyboardInteractiveCallback == nil && (config.GSSAPIWithMICConfig == nil ||
config.GSSAPIWithMICConfig.AllowLogin == nil || config.GSSAPIWithMICConfig.Server == nil) {
config.KeyboardInteractiveCallback == nil && !gssapiWithMICConfigured(config.GSSAPIWithMICConfig) {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
@@ -527,6 +613,15 @@ func (b *BannerError) Error() string {
return b.Err.Error()
}
// maxAuthServerAttempts caps the total number of SSH_MSG_USERAUTH_REQUEST
// messages the server will process on a single connection, regardless of
// outcome (failure, partial success, public key query, or none). It is a
// backstop against clients that drive the authentication loop indefinitely
// without ever incurring a real failure — for example by repeatedly
// triggering PartialSuccessError or by spamming public key offer queries —
// neither of which increment the MaxAuthTries failure counter.
const maxAuthServerAttempts = 128
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
if config.PreAuthConnCallback != nil {
config.PreAuthConnCallback(s)
@@ -537,6 +632,7 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
var perms *Permissions
authFailures := 0
authAttempts := 0
noneAuthCount := 0
var authErrs []error
var calledBannerCallback bool
@@ -565,6 +661,19 @@ userAuthLoop:
return nil, &ServerAuthError{Errors: authErrs}
}
if authAttempts >= maxAuthServerAttempts {
discMsg := &disconnectMsg{
Reason: 2,
Message: "too many authentication attempts",
}
if err := s.transport.writePacket(Marshal(discMsg)); err != nil {
return nil, err
}
authErrs = append(authErrs, discMsg)
return nil, &ServerAuthError{Errors: authErrs}
}
authAttempts++
var userAuthReq userAuthRequestMsg
if packet, err := s.transport.readPacket(); err != nil {
if err == io.EOF {
@@ -737,8 +846,15 @@ userAuthLoop:
}
signedData := buildDataSignedForAuth(sessionID, userAuthReq, algo, pubKeyData)
if err := pubKey.Verify(signedData, sig); err != nil {
// pubKey is reused below for VerifiedPublicKeyCallback and
// must remain the key as presented by the client; derive a
// separate value for Verify that carries any applicable
// no-touch-required opt-out.
pubKeyForVerify := pubKey
if noTouchAllowed(pubKey, candidate.perms) {
pubKeyForVerify = skKeyWithoutUP(pubKey)
}
if err := pubKeyForVerify.Verify(signedData, sig); err != nil {
return nil, err
}
@@ -750,9 +866,16 @@ userAuthLoop:
// considered verified and the callback must not run.
perms, authErr = config.VerifiedPublicKeyCallback(s, pubKey, perms, algo)
}
if authErr == nil && perms != nil && perms.CriticalOptions != nil {
if saco := perms.CriticalOptions[sourceAddressCriticalOption]; saco != "" {
if err := checkSourceAddress(s.RemoteAddr(), saco); err != nil {
authErr = err
}
}
}
}
case "gssapi-with-mic":
if authConfig.GSSAPIWithMICConfig == nil {
if !gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
authErr = errors.New("ssh: gssapi-with-mic auth not configured")
break
}
@@ -824,6 +947,13 @@ userAuthLoop:
var failureMsg userAuthFailureMsg
if partialSuccess, ok := authErr.(*PartialSuccessError); ok {
// Permissions are not preserved between authentication steps. To
// avoid confusion about the final state of the connection, we
// disallow returning non-nil Permissions combined with
// PartialSuccessError.
if perms != nil {
return nil, errors.New("ssh: permissions must be nil when returning PartialSuccessError")
}
// After a partial success error we don't allow changing the user
// name and execute the NoClientAuthCallback.
partialSuccessReturned = true
@@ -878,8 +1008,7 @@ userAuthLoop:
if authConfig.KeyboardInteractiveCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
}
if authConfig.GSSAPIWithMICConfig != nil && authConfig.GSSAPIWithMICConfig.Server != nil &&
authConfig.GSSAPIWithMICConfig.AllowLogin != nil {
if gssapiWithMICConfigured(authConfig.GSSAPIWithMICConfig) {
failureMsg.Methods = append(failureMsg.Methods, "gssapi-with-mic")
}
+3
View File
@@ -423,6 +423,9 @@ func (s *Session) wait(reqs <-chan *Request) error {
for msg := range reqs {
switch msg.Type {
case "exit-status":
if len(msg.Payload) < 4 {
return errors.New("ssh: malformed exit-status request")
}
wm.status = int(binary.BigEndian.Uint32(msg.Payload))
case "exit-signal":
var sigval struct {
+3 -5
View File
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"unicode"
@@ -105,8 +106,7 @@ func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line {
if hint == nil {
// If no hint given, add to the last statement of the given type.
Loop:
for i := len(x.Stmt) - 1; i >= 0; i-- {
stmt := x.Stmt[i]
for _, stmt := range slices.Backward(x.Stmt) {
switch stmt := stmt.(type) {
case *Line:
if stmt.Token != nil && stmt.Token[0] == tokens[0] {
@@ -718,9 +718,7 @@ func (in *input) assignComments() {
}
// Assign suffix comments to syntax immediately before.
for i := len(in.post) - 1; i >= 0; i-- {
x := in.post[i]
for _, x := range slices.Backward(in.post) {
start, end := x.Span()
if debug {
fmt.Fprintf(os.Stderr, "post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte)
+56 -9
View File
@@ -327,6 +327,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parse
}
var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))?([a-z]+[0-9]+)?$`)
var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`)
// Toolchains must be named beginning with `go1`,
@@ -1272,6 +1273,17 @@ func (f *File) SetRequire(req []*Require) {
// SetRequireSeparateIndirect will split it into a direct-only and indirect-only
// block. This aids in the transition to separate blocks.
func (f *File) SetRequireSeparateIndirect(req []*Require) {
f.setRequireSeparateIndirect(req, false)
}
// SetRequireAtMostTwo is like SetRequireSeparateIndirect but it aggressively
// consolidates all requirements into at most two blocks (one direct, one indirect).
// It ignores existing blocks and comments when deciding where to place requirements.
func (f *File) SetRequireAtMostTwo(req []*Require) {
f.setRequireSeparateIndirect(req, true)
}
func (f *File) setRequireSeparateIndirect(req []*Require, simplify bool) {
// hasComments returns whether a line or block has comments
// other than "indirect".
hasComments := func(c Comments) bool {
@@ -1304,6 +1316,17 @@ func (f *File) SetRequireSeparateIndirect(req []*Require) {
}
// Examine existing require lines and blocks.
need := make(map[string]*Require)
for _, r := range req {
need[r.Mod.Path] = r
}
lineIndirect := make(map[*Line]bool)
for _, r := range f.Require {
if n := need[r.Mod.Path]; n != nil {
lineIndirect[r.Syntax] = n.Indirect
}
}
var (
// We may insert new requirements into the last uncommented
// direct-only and indirect-only blocks. We may also move requirements
@@ -1321,7 +1344,9 @@ func (f *File) SetRequireSeparateIndirect(req []*Require) {
// Track the block each requirement belongs to (if any) so we can
// move them later.
lineToBlock = make(map[*Line]*LineBlock)
lineToBlock = make(map[*Line]*LineBlock)
directBlockComments []Comment
indirectBlockComments []Comment
)
for i, stmt := range f.Syntax.Stmt {
switch stmt := stmt.(type) {
@@ -1364,6 +1389,24 @@ func (f *File) SetRequireSeparateIndirect(req []*Require) {
if allIndirect {
lastIndirectIndex = i
}
if simplify {
anyDirect := false
for _, line := range stmt.Line {
if ind, ok := lineIndirect[line]; ok && !ind {
anyDirect = true
break
}
}
target := &directBlockComments
if !anyDirect && len(stmt.Line) > 0 {
target = &indirectBlockComments
}
if len(*target) > 0 && len(stmt.Comments.Before) > 0 {
*target = append(*target, Comment{Token: "//"})
}
*target = append(*target, stmt.Comments.Before...)
stmt.Comments.Before = nil
}
}
}
@@ -1422,6 +1465,15 @@ func (f *File) SetRequireSeparateIndirect(req []*Require) {
lastIndirectBlock = ensureBlock(lastIndirectIndex)
}
if simplify {
if len(directBlockComments) > 0 {
lastDirectBlock.Comments.Before = append(lastDirectBlock.Comments.Before, directBlockComments...)
}
if len(indirectBlockComments) > 0 {
lastIndirectBlock.Comments.Before = append(lastIndirectBlock.Comments.Before, indirectBlockComments...)
}
}
// Delete requirements we don't want anymore.
// Update versions and indirect comments on requirements we want to keep.
// If a requirement is in last{Direct,Indirect}Block with the wrong
@@ -1430,10 +1482,6 @@ func (f *File) SetRequireSeparateIndirect(req []*Require) {
// correct block.
//
// Some blocks may be empty after this. Cleanup will remove them.
need := make(map[string]*Require)
for _, r := range req {
need[r.Mod.Path] = r
}
have := make(map[string]*Require)
for _, r := range f.Require {
path := r.Mod.Path
@@ -1446,10 +1494,10 @@ func (f *File) SetRequireSeparateIndirect(req []*Require) {
r.setVersion(need[path].Mod.Version)
r.setIndirect(need[path].Indirect)
if need[path].Indirect &&
(oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) {
(simplify || oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastDirectBlock) {
moveReq(r, lastIndirectBlock)
} else if !need[path].Indirect &&
(oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) {
(simplify || oneFlatUncommentedBlock || lineToBlock[r.Syntax] == lastIndirectBlock) {
moveReq(r, lastDirectBlock)
}
}
@@ -1736,8 +1784,7 @@ func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace, to
// Remove duplicate replacements.
// Later replacements take priority over earlier ones.
haveReplace := make(map[module.Version]bool)
for i := len(*replace) - 1; i >= 0; i-- {
x := (*replace)[i]
for _, x := range slices.Backward(*replace) {
if haveReplace[x.Old] {
kill[x.Syntax] = true
continue
+16
View File
@@ -10,9 +10,11 @@ package http2
import (
"context"
"crypto/tls"
"errors"
"net"
"net/http"
"slices"
"sync"
"time"
)
@@ -44,6 +46,20 @@ func configureServer(s *http.Server, conf *Server) error {
h2.IdleTimeout = h1.ReadTimeout
}
}
// Register h2 and http/1.1 ALPN protocols on s.TLSConfig, matching
// the pre-wrapping implementation in server.go, so that TLS listeners
// built from s.TLSConfig still negotiate HTTP/2.
if s.TLSConfig == nil {
s.TLSConfig = new(tls.Config)
}
if !slices.Contains(s.TLSConfig.NextProtos, NextProtoTLS) {
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, NextProtoTLS)
}
if !slices.Contains(s.TLSConfig.NextProtos, "http/1.1") {
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "http/1.1")
}
conf.state = &serverInternalState{
s1: s,
}
+13 -2
View File
@@ -22,8 +22,8 @@ import (
)
func configureTransport(t1 *http.Transport) error {
// ConfigureTransport is a no-op: The http.Transport already supports HTTP/2.
return nil
_, err := configureTransports(t1)
return err
}
func configureTransports(t1 *http.Transport) (*Transport, error) {
@@ -31,6 +31,17 @@ func configureTransports(t1 *http.Transport) (*Transport, error) {
// linked to the http.Transport's.
tr2 := &Transport{}
tr2.configure(t1)
// Enable HTTP/2 on the transport, as the pre-wrapping implementation did:
// net/http does not auto-enable it for a transport with a custom
// TLSClientConfig or dialer.
if t1.TLSClientConfig == nil {
t1.TLSClientConfig = &tls.Config{}
}
if t1.Protocols == nil {
t1.Protocols = new(http.Protocols)
t1.Protocols.SetHTTP1(true)
}
t1.Protocols.SetHTTP2(true)
return tr2, nil
}
+1 -1
View File
@@ -109,7 +109,7 @@ func (g *Group) TryGo(f func() error) bool {
if g.sem != nil {
select {
case g.sem <- token{}:
// Note: this allows barging iff channels in general allow barging.
// Note: this allows barging if and only if channels in general allow barging.
default:
return false
}
+76
View File
@@ -6397,3 +6397,79 @@ const (
MPOL_PREFERRED_MANY = 0x5
MPOL_WEIGHTED_INTERLEAVE = 0x6
)
const (
GPIO_V2_GET_LINEINFO_IOCTL = 0xc100b405
GPIO_V2_GET_LINE_IOCTL = 0xc250b407
GPIO_V2_LINE_GET_VALUES_IOCTL = 0xc010b40e
GPIO_V2_LINE_SET_VALUES_IOCTL = 0xc010b40f
GPIO_V2_GET_LINEINFO_WATCH_IOCTL = 0xc100b406
GPIO_GET_LINEINFO_UNWATCH_IOCTL = 0xc004b40c
)
const (
GPIO_V2_LINE_ATTR_ID_FLAGS = 0x1
GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES = 0x2
GPIO_V2_LINE_ATTR_ID_DEBOUNCE = 0x3
GPIO_V2_LINE_CHANGED_REQUESTED = 0x1
GPIO_V2_LINE_CHANGED_RELEASED = 0x2
GPIO_V2_LINE_CHANGED_CONFIG = 0x3
GPIO_V2_LINE_EVENT_RISING_EDGE = 0x1
GPIO_V2_LINE_EVENT_FALLING_EDGE = 0x2
)
type GPIOChipInfo struct {
Name [32]byte
Label [32]byte
Lines uint32
}
type GPIOV2LineValues struct {
Bits uint64
Mask uint64
}
type GPIOV2LineAttribute struct {
Id uint32
_ uint32
Flags uint64
}
type GPIOV2LineConfigAttribute struct {
Attr GPIOV2LineAttribute
Mask uint64
}
type GPIOV2LineConfig struct {
Flags uint64
Num_attrs uint32
_ [5]uint32
Attrs [10]GPIOV2LineConfigAttribute
}
type GPIOV2LineRequest struct {
Offsets [64]uint32
Consumer [32]byte
Config GPIOV2LineConfig
Num_lines uint32
Event_buffer_size uint32
_ [5]uint32
Fd int32
}
type GPIOV2LineInfo struct {
Name [32]byte
Consumer [32]byte
Offset uint32
Num_attrs uint32
Flags uint64
Attrs [10]GPIOV2LineAttribute
_ [4]uint32
}
type GPIOV2LineInfoChanged struct {
Info GPIOV2LineInfo
Timestamp_ns uint64
Event_type uint32
_ [5]uint32
}
type GPIOV2LineEvent struct {
Timestamp_ns uint64
Id uint32
Offset uint32
Seqno uint32
Line_seqno uint32
_ [6]uint32
}
+4
View File
@@ -711,3 +711,7 @@ type SysvShmDesc struct {
_ uint32
_ uint32
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x8044b401
)
+4
View File
@@ -725,3 +725,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x8044b401
)
+4
View File
@@ -705,3 +705,7 @@ type SysvShmDesc struct {
_ uint32
_ uint32
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x8044b401
)
+4
View File
@@ -704,3 +704,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x8044b401
)
+4
View File
@@ -705,3 +705,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x8044b401
)
+4
View File
@@ -710,3 +710,7 @@ type SysvShmDesc struct {
Ctime_high uint16
_ uint16
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+4
View File
@@ -707,3 +707,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+4
View File
@@ -707,3 +707,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+4
View File
@@ -710,3 +710,7 @@ type SysvShmDesc struct {
Ctime_high uint16
_ uint16
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+4
View File
@@ -718,3 +718,7 @@ type SysvShmDesc struct {
_ uint32
_ [4]byte
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+4
View File
@@ -713,3 +713,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+4
View File
@@ -713,3 +713,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+4
View File
@@ -792,3 +792,7 @@ const (
RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE = 0x6
RISCV_HWPROBE_WHICH_CPUS = 0x1
)
const (
GPIO_GET_CHIPINFO_IOCTL = 0x8044b401
)
+4
View File
@@ -727,3 +727,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x8044b401
)
+4
View File
@@ -708,3 +708,7 @@ type SysvShmDesc struct {
_ uint64
_ uint64
}
const (
GPIO_GET_CHIPINFO_IOCTL = 0x4044b401
)
+1 -1
View File
@@ -249,7 +249,7 @@ func upper(c *context) bool {
return c.copy()
}
// isUpper writes the isUppercase version of the current rune to dst.
// isUpper reports whether the current rune is in upper case.
func isUpper(c *context) bool {
ct := c.caseType()
if c.info&hasMappingMask == 0 || ct == cUpper {
+2 -2
View File
@@ -774,7 +774,7 @@ func nlTitle(c *context) bool {
// From CLDR:
// # Special titlecasing for Dutch initial "ij".
// ::Any-Title();
// # Fix up Ij at the beginning of a "word" (per Any-Title, notUAX #29)
// # Fix up Ij at the beginning of a "word" (per Any-Title, not UAX #29)
// [:^WB=ALetter:] [:WB=Extend:]* [[:WB=MidLetter:][:WB=MidNumLet:]]? { Ij } → IJ ;
if c.src[c.pSrc] != 'I' && c.src[c.pSrc] != 'i' {
return title(c)
@@ -794,7 +794,7 @@ func nlTitleSpan(c *context) bool {
// From CLDR:
// # Special titlecasing for Dutch initial "ij".
// ::Any-Title();
// # Fix up Ij at the beginning of a "word" (per Any-Title, notUAX #29)
// # Fix up Ij at the beginning of a "word" (per Any-Title, not UAX #29)
// [:^WB=ALetter:] [:WB=Extend:]* [[:WB=MidLetter:][:WB=MidNumLet:]]? { Ij } → IJ ;
if c.src[c.pSrc] != 'I' {
return isTitle(c)
+8 -1
View File
@@ -121,8 +121,12 @@ func (p Properties) BoundaryAfter() bool {
//
// When all 6 bits are zero, the character is inert, meaning it is never
// influenced by normalization.
//
// We set flags to 0x80 (high bit 7 unused in quick check data) to indicate an invalid rune.
type qcInfo uint8
func (p Properties) isInvalid() bool { return p.flags == 0x80 }
func (p Properties) isYesC() bool { return p.flags&0x10 == 0 }
func (p Properties) isYesD() bool { return p.flags&0x4 == 0 }
@@ -247,6 +251,9 @@ func (f Form) PropertiesString(s string) Properties {
// to a Properties. See the comment at the top of the file
// for more information on the format.
func compInfo(v uint16, sz int) Properties {
if sz == 0 {
return Properties{flags: 0x80, size: 1}
}
if v == 0 {
return Properties{size: uint8(sz)}
} else if v >= 0x8000 {
@@ -254,7 +261,7 @@ func compInfo(v uint16, sz int) Properties {
size: uint8(sz),
ccc: uint8(v),
tccc: uint8(v),
flags: qcInfo(v >> 8),
flags: qcInfo(v>>8) & 0x3f,
}
if p.ccc > 0 || p.combinesBackward() {
p.nLead = uint8(p.flags & 0x3)
+2 -6
View File
@@ -376,16 +376,12 @@ func nextComposed(i *Iter) []byte {
goto doNorm
}
prevCC = i.info.tccc
sz := int(i.info.size)
if sz == 0 {
sz = 1 // illegal rune: copy byte-by-byte
}
p := outp + sz
p := outp + int(i.info.size)
if p > len(i.buf) {
break
}
outp = p
i.p += sz
i.p += int(i.info.size)
if i.p >= i.rb.nsrc {
i.setDone()
break
+10 -10
View File
@@ -148,7 +148,7 @@ func (f Form) IsNormalString(s string) bool {
// patched buffer and whether the decomposition is still in progress.
func patchTail(rb *reorderBuffer) bool {
info, p := lastRuneStart(&rb.f, rb.out)
if p == -1 || info.size == 0 {
if p == -1 || info.isInvalid() {
return true
}
end := p + int(info.size)
@@ -225,7 +225,7 @@ func doAppend(rb *reorderBuffer, out []byte, p int) []byte {
}
fd := &rb.f
if doMerge {
var info Properties
info := Properties{flags: 0x80, size: 1} // invalid rune
if p < n {
info = fd.info(src, p)
if !info.BoundaryBefore() || info.nLeadingNonStarters() > 0 {
@@ -235,7 +235,7 @@ func doAppend(rb *reorderBuffer, out []byte, p int) []byte {
p = decomposeSegment(rb, p, true)
}
}
if info.size == 0 {
if info.isInvalid() {
rb.doFlush()
// Append incomplete UTF-8 encoding.
return src.appendSlice(rb.out, p, n)
@@ -314,7 +314,7 @@ func (f *formInfo) quickSpan(src input, i, end int, atEOF bool) (n int, ok bool)
continue
}
info := f.info(src, i)
if info.size == 0 {
if info.isInvalid() {
if atEOF {
// include incomplete runes
return n, true
@@ -379,7 +379,7 @@ func (f Form) firstBoundary(src input, nsrc int) int {
// CGJ insertion points correctly. Luckily it doesn't have to.
for {
info := fd.info(src, i)
if info.size == 0 {
if info.isInvalid() {
return -1
}
if s := ss.next(info); s != ssSuccess {
@@ -424,7 +424,7 @@ func (f Form) nextBoundary(src input, nsrc int, atEOF bool) int {
}
fd := formTable[f]
info := fd.info(src, 0)
if info.size == 0 {
if info.isInvalid() {
if atEOF {
return 1
}
@@ -435,7 +435,7 @@ func (f Form) nextBoundary(src input, nsrc int, atEOF bool) int {
for i := int(info.size); i < nsrc; i += int(info.size) {
info = fd.info(src, i)
if info.size == 0 {
if info.isInvalid() {
if atEOF {
return i
}
@@ -465,7 +465,7 @@ func lastBoundary(fd *formInfo, b []byte) int {
if p == -1 {
return -1
}
if info.size == 0 { // ends with incomplete rune
if info.isInvalid() { // ends with incomplete rune
if p == 0 { // starts with incomplete rune
return -1
}
@@ -504,7 +504,7 @@ func lastBoundary(fd *formInfo, b []byte) int {
func decomposeSegment(rb *reorderBuffer, sp int, atEOF bool) int {
// Force one character to be consumed.
info := rb.f.info(rb.src, sp)
if info.size == 0 {
if info.isInvalid() {
return 0
}
if s := rb.ss.next(info); s == ssStarter {
@@ -528,7 +528,7 @@ func decomposeSegment(rb *reorderBuffer, sp int, atEOF bool) int {
break
}
info = rb.f.info(rb.src, sp)
if info.size == 0 {
if info.isInvalid() {
if !atEOF {
return int(iShortSrc)
}
+14 -10
View File
@@ -12,7 +12,7 @@ import (
"reflect"
)
// A Kind describes a field of an ast.Node struct.
// A Kind describes a field of an [ast.Node] struct.
type Kind uint8
// String returns a description of the edge kind.
@@ -41,21 +41,25 @@ func (k Kind) Get(n ast.Node, idx int) ast.Node {
panic(fmt.Sprintf("%v.Get(%T): invalid node type", k, n))
}
v := reflect.ValueOf(n).Elem().Field(fieldInfos[k].index)
if idx != -1 {
v = v.Index(idx) // asserts valid index
} else {
// (The type assertion below asserts that v is not a slice.)
if v.Kind() == reflect.Slice {
v = v.Index(idx) // asserts valid idx
} else if idx != -1 {
panic(fmt.Sprintf("%v, Get(%T, %d): cannot index non-slice", v, n, idx))
}
return v.Interface().(ast.Node) // may be nil
out, _ := v.Interface().(ast.Node) // may be nil
return out
}
// Each [Kind] is named Type_Field, where Type is the
// [ast.Node] struct type and Field is the name of the field
const (
Invalid Kind = iota // for nodes at the root of the traversal
// Kinds are sorted alphabetically.
// Numbering is not stable.
// Each is named Type_Field, where Type is the
// ast.Node struct type and Field is the name of the field
// As of Go1.26 these kinds are sorted alphabetically, but
// numbering must be stable, so any new addition of const should
// use a new value (be added at the end of the list).
ArrayType_Elt
ArrayType_Len
+13 -4
View File
@@ -207,11 +207,10 @@ func goListDriver(cfg *Config, runner *gocommand.Runner, overlay string, pattern
// doesn't exist.
extractQueries:
for _, pattern := range patterns {
eqidx := strings.Index(pattern, "=")
if eqidx < 0 {
query, value, ok := strings.Cut(pattern, "=")
if !ok {
restPatterns = append(restPatterns, pattern)
} else {
query, value := pattern[:eqidx], pattern[eqidx+len("="):]
switch query {
case "file":
containFiles = append(containFiles, value)
@@ -563,8 +562,18 @@ func (state *golistState) createDriverResponse(words ...string) (*DriverResponse
} else {
// golang/go#38990: go list silently fails to do cgo processing
pkg.CompiledGoFiles = nil
var msg strings.Builder
fmt.Fprintf(&msg, "go list failed to return CompiledGoFiles for %q.\n", p.Name)
for _, err := range p.DepsErrors {
msg.WriteString(strings.TrimSpace(err.Err))
msg.WriteByte('\n')
}
msg.WriteString("This may indicate failure to perform cgo processing; try building at the command line. See https://golang.org/issue/38990.")
pkg.Errors = append(pkg.Errors, Error{
Msg: "go list failed to return CompiledGoFiles. This may indicate failure to perform cgo processing; try building at the command line. See https://golang.org/issue/38990.",
Msg: msg.String(),
Kind: ListError,
})
}
+28 -2
View File
@@ -539,6 +539,11 @@ type Package struct {
// depsErrors is the DepsErrors field from the go list response, if any.
depsErrors []*packagesinternal.PackageError
// exportDataError is the error encountered reading export data, if any.
// Decoding export data should ordinarily be infallible, so this typically
// indicates a producer/consumer version skew.
exportDataError error
}
// Module provides module information for a package.
@@ -810,6 +815,12 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) {
needsrc: needsrc,
goVersion: response.GoVersion,
}
// Don't trust the driver to respond with duplicate-free
// package names (go.dev/issue/63822).
if _, ok := ld.pkgs[lpkg.ID]; ok {
return nil, fmt.Errorf("%s response contained duplicate packages for ID %q",
cond(ld.externalDriver, "go/packages driver", "go list"), lpkg.ID)
}
ld.pkgs[lpkg.ID] = lpkg
if rootIndex >= 0 {
initial[rootIndex] = lpkg
@@ -1073,10 +1084,11 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) {
}
// TODO(adonovan): this condition looks wrong:
// I think it should be lpkg.needtypes && !lpg.needsrc,
// I think it should be lpkg.needtypes && !lpkg.needsrc,
// so that NeedSyntax without NeedTypes can be satisfied by export data.
if !lpkg.needsrc {
if err := ld.loadFromExportData(lpkg); err != nil {
lpkg.exportDataError = err
lpkg.Errors = append(lpkg.Errors, Error{
Pos: "-",
Msg: err.Error(),
@@ -1215,7 +1227,13 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) {
if ipkg.Types != nil && ipkg.Types.Complete() {
return ipkg.Types, nil
}
log.Fatalf("internal error: package %q without types was imported from %q", path, lpkg)
// If types are unavailable, there must be an export data error.
if ipkg.exportDataError != nil {
return nil, ipkg.exportDataError
}
log.Fatalf("internal error: expected complete types for package %q", path)
panic("unreachable")
})
@@ -1577,3 +1595,11 @@ func usesExportData(cfg *Config) bool {
}
type unit struct{}
func cond[T any](cond bool, t, f T) T {
if cond {
return t
} else {
return f
}
}
+366 -237
View File
@@ -24,8 +24,10 @@
package objectpath
import (
"encoding/binary"
"fmt"
"go/types"
"slices"
"strconv"
"strings"
@@ -124,7 +126,66 @@ func For(obj types.Object) (Path, error) {
// An Encoder amortizes the cost of encoding the paths of multiple objects.
// The zero value of an Encoder is ready to use.
type Encoder struct {
scopeMemo map[*types.Scope][]types.Object // memoization of scopeObjects
pkgIndex map[*types.Package]*pkgIndex
}
// A traversal encapsulates the state of a single traversal of the object/type graph.
type traversal struct {
pkg *types.Package
ix *pkgIndex // non-nil if we are building the index
target types.Object // the sought symbol (if ix == nil)
found Path // the found path (if ix == nil)
// These maps are used to short circuit cycles through
// interface methods, such as occur in the following example:
//
// type I interface { f() interface{I} }
//
// See golang/go#68046 for details.
seenTParamNames map[*types.TypeName]bool // global cycle breaking through type parameters
seenMethods map[*types.Func]bool // global cycle breaking through recursive interfaces
}
// A pkgIndex holds a compressed index of objectpaths of all symbols
// (fields, methods, params) requiring search for an entire package.
//
// The first time a search for a given package is requested, we simply
// traverse the type graph for the target object, maintaining the
// current object path as a stack. If we find the target object, we
// save the path and terminate the main loop (but it's not worth
// breaking out of the current recursion).
//
// On the second search (a pkgIndex exists but its data is nil), we
// build an index of the traversal, which we use for all subsequent
// searches.
//
// The traversal index is encoded in the data field as a list of records,
// one per node, in preorder. Records are of two types:
//
// - A record for a package-level object consists of a pair
// (parent, nameIndex uvarint), where parent is zero and
// nameIndex is the index of the object's name in the sorted
// pkg.Scope().Names() slice.
//
// - A record for a nested node (a segment of an object path)
// consists of (parent uvarint, op byte, index uvarint), where
// parent is the index of the record for the parent node,
// op is the destructuring operator, and index (if op = [AFMTr])
// is its integer operand.
//
// Since data[0] = 0 all nodes have positive offsets. In effect the
// encoding is a trie in which each node stores one path segment
// and points to the node for its prefix.
//
// TODO(adonovan): opt: evaluate an only 2-level tree with nodes for
// package-level objects and the-rest-of-the-path. One calculation
// suggested that it might be similar speed but 30% more compact.
type pkgIndex struct {
pkg *types.Package
data []byte // encoding of traversal; nil if not yet constructed
scopeNames []string // memo of pkg.Scope().Names() to avoid O(n) alloc/sort at lookup
offsets map[types.Object]uint32 // each object's node offset within encoded traversal data
}
// For returns the path to an object relative to its package,
@@ -211,10 +272,9 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
if pkg == nil {
return "", fmt.Errorf("predeclared %s has no path", obj)
}
scope := pkg.Scope()
// 2. package-level object?
if scope.Lookup(obj.Name()) == obj {
if pkg.Scope().Lookup(obj.Name()) == obj {
// Only exported objects (and non-exported types) have a path.
// Non-exported types may be referenced by other objects.
if _, ok := obj.(*types.TypeName); !ok && !obj.Exported() {
@@ -232,19 +292,18 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
// have a path.
return "", fmt.Errorf("no path for %v", obj)
}
case *types.Const, // Only package-level constants have a path.
*types.Label, // Labels are function-local.
*types.PkgName: // PkgNames are file-local.
return "", fmt.Errorf("no path for %v", obj)
case *types.Var:
// Could be:
// - a field (obj.IsField())
// - a func parameter or result
// - a local var.
// Sadly there is no way to distinguish
// a param/result from a local
// so we must proceed to the find.
// A var, if not package-level, must be a
// parameter (incl. receiver) or result, or a struct field.
if obj.Kind() == types.LocalVar {
return "", fmt.Errorf("no path for local %v", obj)
}
case *types.Func:
// A func, if not package-level, must be a method.
@@ -261,89 +320,311 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
panic(obj)
}
// 4. Search the API for the path to the var (field/param/result) or method.
// 4. Search the object/type graph for the path to
// the var (field/param/result) or method.
ix, ok := enc.pkgIndex[pkg]
if !ok {
// First search: don't build an index, just traverse.
// This avoids allocation in [For], whose Encoder
// lives for a single call.
ix = &pkgIndex{pkg: pkg}
// First inspect package-level named types.
// In the presence of path aliases, these give
// the best paths because non-types may
// refer to types, but not the reverse.
empty := make([]byte, 0, 48) // initial space
objs := enc.scopeObjects(scope)
for _, o := range objs {
tname, ok := o.(*types.TypeName)
if !ok {
continue // handle non-types in second pass
if enc.pkgIndex == nil {
enc.pkgIndex = make(map[*types.Package]*pkgIndex)
}
enc.pkgIndex[pkg] = ix // build the index next time
f := traversal{pkg: pkg, target: obj}
f.traverse()
if f.found != "" {
return f.found, nil
}
} else {
// Second search: build an index while traversing.
if ix.data == nil {
ix.offsets = make(map[types.Object]uint32)
ix.data = []byte{0} // offset 0 is sentinel
(&traversal{pkg: pkg, ix: ix}).traverse()
}
path := append(empty, o.Name()...)
path = append(path, opType)
T := o.Type()
if alias, ok := T.(*types.Alias); ok {
if r := findTypeParam(obj, alias.TypeParams(), path, opTypeParam); r != nil {
return Path(r), nil
}
if r := find(obj, alias.Rhs(), append(path, opRhs)); r != nil {
return Path(r), nil
}
} else if tname.IsAlias() {
// legacy alias
if r := find(obj, T, path); r != nil {
return Path(r), nil
}
} else if named, ok := T.(*types.Named); ok {
// defined (named) type
if r := findTypeParam(obj, named.TypeParams(), path, opTypeParam); r != nil {
return Path(r), nil
}
if r := find(obj, named.Underlying(), append(path, opUnderlying)); r != nil {
return Path(r), nil
}
}
}
// Then inspect everything else:
// non-types, and declared methods of defined types.
for _, o := range objs {
path := append(empty, o.Name()...)
if _, ok := o.(*types.TypeName); !ok {
if o.Exported() {
// exported non-type (const, var, func)
if r := find(obj, o.Type(), append(path, opType)); r != nil {
return Path(r), nil
}
}
continue
}
// Inspect declared methods of defined types.
if T, ok := types.Unalias(o.Type()).(*types.Named); ok {
path = append(path, opType)
// The method index here is always with respect
// to the underlying go/types data structures,
// which ultimately derives from source order
// and must be preserved by export data.
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
path2 := appendOpArg(path, opMethod, i)
if m == obj {
return Path(path2), nil // found declared method
}
if r := find(obj, m.Type(), append(path2, opType)); r != nil {
return Path(r), nil
}
}
// Second and later searches: consult the index.
if offset, ok := ix.offsets[obj]; ok {
return ix.path(offset), nil
}
}
return "", fmt.Errorf("can't find path for %v in %s", obj, pkg.Path())
}
func appendOpArg(path []byte, op byte, arg int) []byte {
// traverse performs a complete traversal of all symbols reachable from the package.
func (tr *traversal) traverse() {
scope := tr.pkg.Scope()
names := scope.Names()
if tr.ix != nil {
tr.ix.scopeNames = names
}
empty := make([]byte, 0, 48) // initial space for stack (ix == nil)
// First inspect package-level type names.
// In the presence of path aliases, these give
// the best paths because non-types may
// refer to types, but not the reverse.
for i, name := range names {
if tr.found != "" {
return // found (ix == nil)
}
obj := scope.Lookup(name)
if _, ok := obj.(*types.TypeName); !ok {
continue // handle non-types in second pass
}
// emit (name, opType)
var path []byte
var offset uint32
if tr.ix == nil {
path = append(empty, name...)
path = append(path, opType)
} else {
offset = tr.ix.emitPackageLevel(i)
tr.ix.offsets[obj] = offset
offset = tr.ix.emitPathSegment(offset, opType, -1)
}
// A TypeName (for Named or Alias) may have type parameters.
switch t := obj.Type().(type) {
case *types.Alias:
tr.tparams(t.TypeParams(), path, offset, opTypeParam)
tr.typ(path, offset, opRhs, -1, t.Rhs())
case *types.Named:
tr.tparams(t.TypeParams(), path, offset, opTypeParam)
tr.typ(path, offset, opUnderlying, -1, t.Underlying())
}
}
// Then inspect everything else:
// exported non-types, and declared methods of defined types.
for i, name := range names {
if tr.found != "" {
return // found (ix == nil)
}
obj := scope.Lookup(name)
if tname, ok := obj.(*types.TypeName); !ok {
if obj.Exported() {
// exported non-type (const, var, func)
var path []byte
var offset uint32
if tr.ix == nil {
path = append(empty, name...)
} else {
offset = tr.ix.emitPackageLevel(i)
tr.ix.offsets[obj] = offset
}
tr.typ(path, offset, opType, -1, obj.Type())
}
} else if T, ok := types.Unalias(tname.Type()).(*types.Named); ok {
// defined type
var path []byte
var offset uint32
if tr.ix == nil {
path = append(empty, name...)
path = append(path, opType)
} else {
// Inv: map entry for obj was populated in first pass.
offset = tr.ix.emitPathSegment(tr.ix.offsets[obj], opType, -1)
}
// Inspect declared methods of defined types.
//
// The method index here is always with respect
// to the underlying go/types data structures,
// which ultimately derives from source order
// and must be preserved by export data.
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
tr.object(path, offset, opMethod, i, m)
}
}
}
}
func (tr *traversal) visitType(path []byte, offset uint32, T types.Type) {
switch T := T.(type) {
case *types.Alias:
tr.typ(path, offset, opRhs, -1, T.Rhs())
case *types.Basic, *types.Named:
// Named types belonging to pkg were handled already,
// so T must belong to another package. No path.
return
case *types.Pointer, *types.Slice, *types.Array, *types.Chan:
type hasElem interface{ Elem() types.Type } // note: includes Map
tr.typ(path, offset, opElem, -1, T.(hasElem).Elem())
case *types.Map:
tr.typ(path, offset, opKey, -1, T.Key())
tr.typ(path, offset, opElem, -1, T.Elem())
case *types.Signature:
tr.tparams(T.RecvTypeParams(), path, offset, opRecvTypeParam)
tr.tparams(T.TypeParams(), path, offset, opTypeParam)
tr.typ(path, offset, opParams, -1, T.Params())
tr.typ(path, offset, opResults, -1, T.Results())
case *types.Struct:
for i := 0; i < T.NumFields(); i++ {
tr.object(path, offset, opField, i, T.Field(i))
}
case *types.Tuple:
for i := 0; i < T.Len(); i++ {
tr.object(path, offset, opAt, i, T.At(i))
}
case *types.Interface:
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
if m.Pkg() != nil && m.Pkg() != tr.pkg {
continue // embedded method from another package
}
if !tr.seenMethods[m] {
if tr.seenMethods == nil {
tr.seenMethods = make(map[*types.Func]bool)
}
tr.seenMethods[m] = true
tr.object(path, offset, opMethod, i, m)
}
}
case *types.TypeParam:
tname := T.Obj()
if tname.Pkg() != nil && tname.Pkg() != tr.pkg {
return // type parameter from another package
}
if !tr.seenTParamNames[tname] {
if tr.seenTParamNames == nil {
tr.seenTParamNames = make(map[*types.TypeName]bool)
}
tr.seenTParamNames[tname] = true
tr.object(path, offset, opObj, -1, tname)
tr.typ(path, offset, opConstraint, -1, T.Constraint())
}
}
}
func (tr *traversal) tparams(list *types.TypeParamList, path []byte, offset uint32, op byte) {
for i := 0; i < list.Len(); i++ {
tr.typ(path, offset, op, i, list.At(i))
}
}
// typ descends the type graph edge (op, index), then proceeds to traverse type t.
func (tr *traversal) typ(path []byte, offset uint32, op byte, index int, t types.Type) {
if tr.ix == nil {
path = appendOpArg(path, op, index)
} else {
offset = tr.ix.emitPathSegment(offset, op, index)
}
tr.visitType(path, offset, t)
}
// object descends the type graph edge (op, index), records object
// obj, then proceeds to traverse its type.
func (tr *traversal) object(path []byte, offset uint32, op byte, index int, obj types.Object) {
if tr.ix == nil {
path = appendOpArg(path, op, index)
if obj == tr.target && tr.found == "" {
tr.found = Path(path)
}
path = append(path, opType)
} else {
offset = tr.ix.emitPathSegment(offset, op, index)
if _, ok := tr.ix.offsets[obj]; !ok {
tr.ix.offsets[obj] = offset
}
offset = tr.ix.emitPathSegment(offset, opType, -1)
}
tr.visitType(path, offset, obj.Type())
}
// emitPackageLevel encodes a record for a package-level symbol,
// identified by its index in ix.scopeNames.
func (p *pkgIndex) emitPackageLevel(index int) uint32 {
off := uint32(len(p.data))
p.data = append(p.data, 0) // zero varint => no parent
p.data = binary.AppendUvarint(p.data, uint64(index))
return off
}
// emitPathSegment emits a record for a non-initial object path segment.
func (p *pkgIndex) emitPathSegment(parent uint32, op byte, index int) uint32 {
off := uint32(len(p.data))
p.data = binary.AppendUvarint(p.data, uint64(parent))
p.data = append(p.data, op)
switch op {
case opAt, opField, opMethod, opTypeParam, opRecvTypeParam:
p.data = binary.AppendUvarint(p.data, uint64(index))
}
return off
}
// path returns the Path for the encoded node at the specified offset.
func (p *pkgIndex) path(offset uint32) Path {
var elems []string // path elements in reverse
for {
// Read parent index.
parent, n := binary.Uvarint(p.data[offset:])
offset += uint32(n)
if parent == 0 {
break // root (end of path)
}
op := p.data[offset]
offset++
// The [AFMTr] operators have a numeric operand.
switch op {
case opAt, opField, opMethod, opTypeParam, opRecvTypeParam:
val, n := binary.Uvarint(p.data[offset:])
offset += uint32(n)
elems = append(elems, strconv.Itoa(int(val)))
}
elems = append(elems, string([]byte{op}))
offset = uint32(parent)
}
idx, _ := binary.Uvarint(p.data[offset:])
// Convert index to Path string.
name := p.scopeNames[idx]
sz := len(name)
for _, elem := range elems {
sz += len(elem)
}
var buf strings.Builder
buf.Grow(sz)
buf.WriteString(name)
for _, elem := range slices.Backward(elems) {
buf.WriteString(elem)
}
return Path(buf.String())
}
// appendOpArg appends (op, index) to the object path.
// A negative index is ignored.
func appendOpArg(path []byte, op byte, index int) []byte {
path = append(path, op)
path = strconv.AppendInt(path, int64(arg), 10)
if index >= 0 {
path = strconv.AppendInt(path, int64(index), 10)
}
return path
}
@@ -442,138 +723,6 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) {
// panic(fmt.Sprintf("couldn't find method %s on type %s; methods: %#v", meth, named, enc.namedMethods(named)))
}
// find finds obj within type T, returning the path to it, or nil if not found.
//
// The seen map is used to short circuit cycles through type parameters. If
// nil, it will be allocated as necessary.
//
// The seenMethods map is used internally to short circuit cycles through
// interface methods, such as occur in the following example:
//
// type I interface { f() interface{I} }
//
// See golang/go#68046 for details.
func find(obj types.Object, T types.Type, path []byte) []byte {
return (&finder{obj: obj}).find(T, path)
}
// finder closes over search state for a call to find.
type finder struct {
obj types.Object // the sought object
seenTParamNames map[*types.TypeName]bool // for cycle breaking through type parameters
seenMethods map[*types.Func]bool // for cycle breaking through recursive interfaces
}
func (f *finder) find(T types.Type, path []byte) []byte {
switch T := T.(type) {
case *types.Alias:
return f.find(types.Unalias(T), path)
case *types.Basic, *types.Named:
// Named types belonging to pkg were handled already,
// so T must belong to another package. No path.
return nil
case *types.Pointer:
return f.find(T.Elem(), append(path, opElem))
case *types.Slice:
return f.find(T.Elem(), append(path, opElem))
case *types.Array:
return f.find(T.Elem(), append(path, opElem))
case *types.Chan:
return f.find(T.Elem(), append(path, opElem))
case *types.Map:
if r := f.find(T.Key(), append(path, opKey)); r != nil {
return r
}
return f.find(T.Elem(), append(path, opElem))
case *types.Signature:
if r := f.findTypeParam(T.RecvTypeParams(), path, opRecvTypeParam); r != nil {
return r
}
if r := f.findTypeParam(T.TypeParams(), path, opTypeParam); r != nil {
return r
}
if r := f.find(T.Params(), append(path, opParams)); r != nil {
return r
}
return f.find(T.Results(), append(path, opResults))
case *types.Struct:
for i := 0; i < T.NumFields(); i++ {
fld := T.Field(i)
path2 := appendOpArg(path, opField, i)
if fld == f.obj {
return path2 // found field var
}
if r := f.find(fld.Type(), append(path2, opType)); r != nil {
return r
}
}
return nil
case *types.Tuple:
for i := 0; i < T.Len(); i++ {
v := T.At(i)
path2 := appendOpArg(path, opAt, i)
if v == f.obj {
return path2 // found param/result var
}
if r := f.find(v.Type(), append(path2, opType)); r != nil {
return r
}
}
return nil
case *types.Interface:
for i := 0; i < T.NumMethods(); i++ {
m := T.Method(i)
if f.seenMethods[m] {
continue // break cycles (see TestIssue70418)
}
path2 := appendOpArg(path, opMethod, i)
if m == f.obj {
return path2 // found interface method
}
if f.seenMethods == nil {
f.seenMethods = make(map[*types.Func]bool)
}
f.seenMethods[m] = true
if r := f.find(m.Type(), append(path2, opType)); r != nil {
return r
}
}
return nil
case *types.TypeParam:
name := T.Obj()
if f.seenTParamNames[name] {
return nil
}
if name == f.obj {
return append(path, opObj)
}
if f.seenTParamNames == nil {
f.seenTParamNames = make(map[*types.TypeName]bool)
}
f.seenTParamNames[name] = true
if r := f.find(T.Constraint(), append(path, opConstraint)); r != nil {
return r
}
return nil
}
panic(T)
}
func findTypeParam(obj types.Object, list *types.TypeParamList, path []byte, op byte) []byte {
return (&finder{obj: obj}).findTypeParam(list, path, op)
}
func (f *finder) findTypeParam(list *types.TypeParamList, path []byte, op byte) []byte {
for i := 0; i < list.Len(); i++ {
tparam := list.At(i)
path2 := appendOpArg(path, op, i)
if r := f.find(tparam, path2); r != nil {
return r
}
}
return nil
}
// Object returns the object denoted by path p within the package pkg.
func Object(pkg *types.Package, p Path) (types.Object, error) {
pathstr := string(p)
@@ -708,7 +857,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
}
tparams := hasTypeParams.TypeParams()
if n := tparams.Len(); index >= n {
return nil, fmt.Errorf("tuple index %d out of range [0-%d)", index, n)
return nil, fmt.Errorf("type parameter index %d out of range [0-%d)", index, n)
}
t = tparams.At(index)
@@ -719,7 +868,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
}
rtparams := sig.RecvTypeParams()
if n := rtparams.Len(); index >= n {
return nil, fmt.Errorf("tuple index %d out of range [0-%d)", index, n)
return nil, fmt.Errorf("receiver type parameter index %d out of range [0-%d)", index, n)
}
t = rtparams.At(index)
@@ -794,23 +943,3 @@ func Object(pkg *types.Package, p Path) (types.Object, error) {
return obj, nil // success
}
// scopeObjects is a memoization of scope objects.
// Callers must not modify the result.
func (enc *Encoder) scopeObjects(scope *types.Scope) []types.Object {
m := enc.scopeMemo
if m == nil {
m = make(map[*types.Scope][]types.Object)
enc.scopeMemo = m
}
objs, ok := m[scope]
if !ok {
names := scope.Names() // allocates and sorts
objs = make([]types.Object, len(names))
for i, name := range names {
objs[i] = scope.Lookup(name)
}
m[scope] = objs
}
return objs
}

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