Compare commits

...

61 Commits

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

Migrations:

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

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

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

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

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

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

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

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

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

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

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

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

The end result of these changes is that we now see the correct addresses when you supply targets via `--edge`, while also making the code a bit clearer.
2026-05-14 09:06:02 +00:00
Miguel da Costa Martins Marcelino 31de04f858 TUN-10525: Add prechecks kill switch
Instead of having the  --precheck flag in cloudflared, we allow controlling prechecks via a DNS flag, so we can short-circuit this behavior in case anything goes wrong. Although we don't expect pre-checks to add that much traffic, we should still guarantee that we can stop pre-checks in case something goes wrong.
2026-05-13 18:05:11 +00:00
João "Pisco" Fernandes fbfd76089f fix: Update golang.org/x/net to v0.54.0
Check / check (1.22.x, ubuntu-latest) (push) Failing after 5m15s
Semgrep config / semgrep/ci (push) Failing after 1m19s
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
2026-05-13 13:15:15 +01:00
lneto 21ca2e225e Release 2026.5.0 2026-05-13 11:09:55 +01:00
lneto f674b82e2a TUN-10413: Centralize TLS curve configuration in crypto/ and adopt X25519MLKEM768 for QUIC/H2
Introduce a new crypto/ package as the single source of truth for TLS
curve preferences used on every edge-facing connection, and adopt
X25519MLKEM768 as the primary post-quantum key exchange for both QUIC
and HTTP/2:

  PQ Prefer (default):     X25519MLKEM768, P256Kyber768Draft00, CurveP256
  PQ Strict (--post-quantum): X25519MLKEM768, P256Kyber768Draft00

The curve list is identical under FIPS and non-FIPS builds, so
crypto.GetCurvePreferences takes only a features.PostQuantumMode and
returns a fresh slice on every call.

HTTP/2 now applies these curve preferences the same way QUIC does. The
previous PostQuantumStrict rejection in serveHTTP2 and the forced
QUIC-only selection in NewProtocolSelector are removed since both
transports support the same post-quantum curves; the needPQ parameter
is dropped from NewProtocolSelector accordingly.

Also fix a shared tls.Config race: both the QUIC and HTTP/2 paths now
Clone() the per-protocol entry from TunnelConfig.EdgeTLSConfigs before
mutating CurvePreferences instead of writing through the shared map
entry.

Legacy Kyber draft curve X25519Kyber768Draft00
and the unused removeDuplicates helper are removed along with the old
supervisor/pqtunnels.go / _test.go files.

AGENTS.md is updated with guidance on the new crypto/ package, the
cfdcrypto import alias, the tls.Config cloning rule, and the lint
workflow implications of .golangci.yaml's whole-files: true setting.
2026-05-12 07:47:38 +01:00
MiguelMarcelino ae3799a098 Bump golang.org/x/net from v0.40.0 to v0.53.0
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
2026-05-08 11:13:48 +00:00
João "Pisco" Fernandes 4d8df2b2c0 TUN-10513: Disable /debug/pprof/cmdline endpoint
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
2026-05-07 18:41:38 +01:00
Miguel da Costa Martins Marcelino a67c583bf1 TUN-10390: Call prechecks
Final run method, which runs cloudlflared pre-checks for both the normal startup procedure, as well as cloudflared diag.

For cloudflared diag, this produces a new json output to which is added to the final zip file.

Also added in a new flag to prevent this from running all the time, at least for now until we are 100% sure this works as intended. We will later remove this flag, only leaving in `--no-prechecks`, so this runs by default for everyone using cloudflared.

Tested pre-checks locally with origintunneld. The results show all pre-checks succeeding. In this case, it ran with only 1 region, since locally we run it with `--edge origintunneld1:7844`.

![Screenshot 2026-05-07 at 13.19.19.png](/uploads/8d0031d7c819d8a761707fe9d845667f/Screenshot_2026-05-07_at_13.19.19.png){width=900 height=217}
2026-05-07 17:27:58 +00:00
Miguel da Costa Martins Marcelino 22a955f7bb TUN-10511: Add Static DNS Resolvers
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
This PR allows us to use edge overrides with pre-checks. I forgot to account for the --edge flag when developing pre-check probes in TUN-10388. This should allow us to wire this flag in.

DNSResolver will still be injected via RunDialers, and we will take care of initialization in cmd.go. This allows us to keep pre-checks testable and inject mock DNSResolvers when needed.

I thought about overriding `edgediscovery.ResolveEdge` and `edgeDiscovery.StaticEdge` instead of `allregion.EdgeDiscovery` and `allregionResolveAddrs`, respectively. But that would imply changing our existing probe logic to support `Regions` instead of `[][]EdgeAddr`, which would mean more work. Additionally (and perhaps most importantly), using `Regions` would also require us to create new functions to extract a list of edge addresses for our probe tests. I don't think this would go well with the current implementation of `Regions`, as I believe it's intent is to encapsulate the logic around managing addresses per regions. Adding these functions would mean breaking this encapsulation.
2026-05-07 11:42:01 +00:00
Gonçalo Garcia a453612e7c TUN-10507: Bump go and go-boring to 1.26.2
## What

Bumps go-boring from 1.26.0-1 to 1.26.2-1 and CI builder image from \`3501-fc698419a625\` to \`3595-779e088c0ec4\`.

go1.26.2 (released 2026-04-07) includes security fixes to the \`go\` command, the compiler, and the \`archive/tar\`, \`crypto/tls\`, \`crypto/x509\`, \`html/template\`, and \`os\` packages, as well as bug fixes to the \`net\`, \`net/http\`, and \`net/url\` packages.

### Security fixes (relevant)
- **crypto/tls**: multiple CVEs — cloudflared uses TLS extensively for tunnel connections
- **crypto/x509**: CVE-2026-32280 (excessive chain-building in \`Verify\`), CVE-2026-32281 (quadratic work in policy validation)

### Net bug fixes (not applicable)
- **net/url #78111**: \`url.Parse\` regression for MongoDB-style multi-host URLs — not used in cloudflared
- **net/http #78019**: race condition on Windows when using \`os.File\` as HTTP request body — cloudflared does not pass \`os.File\` as a request body
- **net #77885**: \`ReadMsgUDP\`/\`WriteMsgUDP\` WSAEFAULT on Windows with empty non-nil oob — quic-go uses \`basicConn\` on Windows (\`ReadFrom\`, not \`ReadMsgUDP\`)

## Jira

[TUN-10507](https://jira.cfdata.org/browse/TUN-10507)
2026-05-07 08:39:53 +00:00
Miguel da Costa Martins Marcelino e8f8b2afb7 TUN-10390: Fix missing TLS settings
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Fixing missing TLS settings. While developing the pre-check probes, I forgot to add the certificate settings, which are essential for establishing a connection to origintunneld. I discovered this while testing cloudflared locally.
2026-05-06 11:17:59 +00:00
Miguel da Costa Martins Marcelino 7585e38948 chore: Fix warnings
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Fixing warnings in cloudflared log collector.

This attempts to fix errors like the ones shown below:

```
diagnostic/diagnostic.go:132:23: Error return value of `logHandle.Close` is not checked (errcheck)
	defer logHandle.Close()

diagnostic/diagnostic.go:134:26: G303: File creation in shared tmp directory without using ioutil.Tempfile (gosec)
	outputLogHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
```
2026-05-05 08:28:41 +00:00
Miguel da Costa Martins Marcelino a9b6f703f0 TUN-10389: Implement main run method
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
This introduces  the main precheck logic. This will follow concurrency model, timeout handling, and retry logic established in the SPEC. We will follow the decision flow in the [connectivity pre-checks SPEC](https://wiki.cfdata.org/spaces/TUN/pages/1374967685/Connectivity+Pre-checks+for+cloudflared). You can find an attached image of what the decision flow should look like.

![image.png](/uploads/fa71215adc2da509f6cbbb74532e3d95/image.png){width=900 height=235}
2026-05-04 16:34:52 +00:00
Evan Raw da81fb02ec AUTH-4699, AUTH-8460, TUN-10179: Fix .lock file deletion race condition
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Replace the lock file mechanism with PID+start-time based stale
detection so that no cleanup is required on process death.

When both org and app token locks were held, the first signal handler
to call os.Exit() would kill the process before the second handler
could delete its lock file. The orphaned lock file then caused the
next invocation to wait ~128 seconds in an exponential backoff loop
before forcibly deleting it. The same issue occurred on SIGKILL, OOM,
or any non-signal death.

Lock files now contain the holder's PID and process start time as
JSON. On acquisition, if a lock file already exists, the recorded
process is checked for liveness via gopsutil. Stale locks are
reclaimed immediately with no backoff. Atomic O_CREATE|O_EXCL
prevents races between concurrent acquirers.

Also adds a companion .url file so processes waiting on an active
lock can print the auth URL for the user.
2026-05-01 13:04:51 +00:00
Evan Raw 23b15d0eb6 AUTH-4699, AUTH-8460, TUN-10179: Vendor gopsutil/v4 for cross-platform process identification 2026-05-01 13:04:51 +00:00
Miguel da Costa Martins Marcelino 4a2cbd1870 TUN-10389: Improve probe functions
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Follow-up to https://gitlab.cfdata.org/cloudflare/tun/cloudflared/-/merge_requests/1819. This applies suggestions from that PR to make the code easier to read and more maintainable.
2026-04-30 16:18:07 +00:00
Miguel da Costa Martins Marcelino 9978cfd0d5 TUN-10388 Implement dialers for connectivity checks
This PR implements all the dialers and resolvers needed to make pre-checks happen. So this task focuses on the following:

1. Implement the DNS probe: call DNSResolver.Resolve(region)
2. Implement the QUIC probe: call QUICDialer.DialQuic (handshake only, no stream opened) and record the result.
3. Implement the HTTP/2 probe: call TCPDialer.DialEdge (TCP + TLS handshake only, no frames sent) and record the result.
4. Implement the Management API probe: call ManagementDialer.DialContext to api.cloudflare.com:443 and record the result.
5. Export edgeDiscovery as EdgeDiscovery in edgediscovery/allregions/discovery.go so the pre-check can reuse the production DNS path.

This sets up the main components to implement the checker.
2026-04-30 15:15:25 +00:00
Miguel da Costa Martins Marcelino a0401df621 TUN-10388: Adding probe check
Adding new probe check for UDP connectivity. This ensures that we skip the connection index when doing probes in cloudflared.
2026-04-30 14:32:24 +01:00
Miguel da Costa Martins Marcelino cf17ba93b2 TUN-10388: Use pointer for suggested protocol
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Switching `SuggestedProtocol` to a pointer, so we can pass nil whenever both QUIC and HTTP2 fail. We should not be suggesting anything when all our protocols have failed.
2026-04-29 13:36:38 +00:00
Miguel da Costa Martins Marcelino f827e6216b chore: Add pre-push hooks
Adding pre-push hooks to cloudflared. While developing in cloudflared, I found myself constantly bumping into issues in CI, as I was forgetting to run linters and tests at times. We should run these before pushing any code to our repo.
2026-04-29 13:09:22 +00:00
Harshini Ramanujam df981b4d89 SECENG-13496 update pkg docs for gokeyless to support multiple builds
* To support older glibc OS - building separate versions for compatibility

Closes SECENG-13496
2026-04-29 05:37:09 -04:00
Miguel da Costa Martins Marcelino ddd76fa05f TUN-10387: Add no-prechecks flag
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Add a new no-prechecks flag to cloudflared. This will allow skipping connectivity pre-checks at startup.
2026-04-27 11:29:43 +00:00
Miguel da Costa Martins Marcelino 9f084e6800 TUN-10386: Add Table Renderer
Check / check (1.22.x, macos-latest) (push) Has been cancelled
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
The goals of this PR are twofold:

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

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

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

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

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

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

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

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

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

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

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

Closes TUN-10292
2026-03-05 16:31:24 +00:00
Luis Neto 649705d291 TUN-10258: add agents.md
Check / check (1.22.x, ubuntu-latest) (push) Has been cancelled
Check / check (1.22.x, windows-latest) (push) Has been cancelled
Semgrep config / semgrep/ci (push) Has been cancelled
Check / check (1.22.x, macos-latest) (push) Has been cancelled
* chore: add agents.md

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

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

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

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

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

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

Closes TUN-10216
2026-02-06 10:01:07 +00:00
João "Pisco" Fernandes d7c62aed71 Release 2026.1.2 2026-01-23 12:45:53 +00:00
João "Pisco" Fernandes 2b95c61044 Revert "TUN-9863: Update pipelines to use cloudflared EV Certificate"
This reverts commit 789a9b110d.
2026-01-23 12:45:36 +00:00
João "Pisco" Fernandes efd0189121 Revert "TUN-9886 notarize cloudflared"
This reverts commit 9abcfece66.
2026-01-21 13:33:53 +00:00
Andi Anderson 9abcfece66 TUN-9886 notarize cloudflared 2026-01-21 12:14:06 +00:00
1638 changed files with 163857 additions and 229236 deletions
+33 -33
View File
@@ -2,38 +2,38 @@ ARG CLOUDFLARE_DOCKER_REGISTRY_HOST
FROM ${CLOUDFLARE_DOCKER_REGISTRY_HOST:-registry.cfdata.org}/stash/cf/debian-images/trixie/main:2026.1.0@sha256:e32092fd01520f5ae7de1fa6bb5a721720900ebeaa48e98f36f6f86168833cd7
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install --no-install-recommends --allow-downgrades -y \
build-essential \
git \
go-boring=1.24.11-1 \
libffi-dev \
procps \
python3-dev \
python3-pip \
python3-setuptools \
python3-venv \
# tool to create msi packages
wixl \
# install ruby and rpm which are required to install fpm package builder
rpm \
ruby \
ruby-dev \
rubygems \
# create deb and rpm repository files
reprepro \
createrepo-c \
# gcc for cross architecture compilation in arm
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross && \
rm -rf /var/lib/apt/lists/* && \
# Install fpm gem
gem install fpm --no-document && \
# Initialize rpm repository, SQL Lite DB
mkdir -p /var/lib/rpm && \
rpm --initdb && \
chmod -R 777 /var/lib/rpm && \
# Create work directory
mkdir -p opt
apt-get upgrade -y && \
apt-get install --no-install-recommends --allow-downgrades -y \
build-essential \
git \
go-boring=1.26.4-1 \
libffi-dev \
procps \
python3-dev \
python3-pip \
python3-setuptools \
python3-venv \
# tool to create msi packages
wixl \
# install ruby and rpm which are required to install fpm package builder
rpm \
ruby \
ruby-dev \
rubygems \
# create deb and rpm repository files
reprepro \
createrepo-c \
# gcc for cross architecture compilation in arm
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross && \
rm -rf /var/lib/apt/lists/* && \
# Install fpm gem
gem install fpm --no-document && \
# Initialize rpm repository, SQL Lite DB
mkdir -p /var/lib/rpm && \
rpm --initdb && \
chmod -R 777 /var/lib/rpm && \
# Create work directory
mkdir -p opt
WORKDIR /opt
+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
+4 -4
View File
@@ -1,11 +1,11 @@
.golang-inputs: &golang_inputs
runOnMR: true
runOnBranches: '^master$'
runOnBranches: "^master$"
outputDir: artifacts
runner: linux-x86-8cpu-16gb
stage: build
golangVersion: "boring-1.24"
imageVersion: "3393-947ec7a@sha256:f81acc2c8ecaa84acb290c43c080702ae3aba6464201a20f9d6eff619be7c878"
golangVersion: "boring-1.26"
imageVersion: "3625-1801d52@sha256:9261597bc2d229c997522848260de758567643d58ae1097196ae368db89a1d0f"
CGO_ENABLED: 1
.default-packaging-job: &packaging-job-defaults
@@ -65,7 +65,7 @@ include:
- component: $CI_SERVER_FQDN/cloudflare/ci/golang/boring-make@~latest
inputs:
<<: *golang_inputs
runOnBranches: '^$'
runOnBranches: "^$"
stage: validate
jobPrefix: vulncheck
GOLANG_MAKE_TARGET: vulncheck
+1 -1
View File
@@ -28,7 +28,7 @@ macos-build-cloudflared: &mac-build
- '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64'
- ARCH=$(uname -m)
- echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH
- ./.ci/scripts/mac/install-go.sh
- ./.ci/scripts/mac/install-go.sh "$MAC_GO_VERSION"
- BUILD_SCRIPT=.ci/scripts/mac/build.sh
- if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi
- set -euo pipefail
+6 -2
View File
@@ -2,9 +2,13 @@ rm -rf /tmp/go
export GOCACHE=/tmp/gocache
rm -rf $GOCACHE
brew install go@1.24
if [ -z "$1" ]
then
echo "No go version supplied"
fi
brew install "$1"
go version
which go
go env
+16 -15
View File
@@ -4,13 +4,14 @@ set -e -u
# Define the file to store the list of vulnerabilities to ignore.
IGNORE_FILE=".vulnignore"
go version
# Check if the ignored vulnerabilities file exists. If not, create an empty one.
if [ ! -f "$IGNORE_FILE" ]; then
touch "$IGNORE_FILE"
echo "Created an empty file to store ignored vulnerabilities: $IGNORE_FILE"
echo "# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line." >> "$IGNORE_FILE"
echo "# You can also add comments on the same line after the ID." >> "$IGNORE_FILE"
echo "" >> "$IGNORE_FILE"
touch "$IGNORE_FILE"
echo "Created an empty file to store ignored vulnerabilities: $IGNORE_FILE"
echo "# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line." >>"$IGNORE_FILE"
echo "# You can also add comments on the same line after the ID." >>"$IGNORE_FILE"
echo "" >>"$IGNORE_FILE"
fi
# Run govulncheck and capture its output.
@@ -31,22 +32,22 @@ echo "====================================="
CLEAN_IGNORES=$(grep -v '^\s*#' "$IGNORE_FILE" | cut -d'#' -f1 | sed 's/ //g' | sort -u || true)
# Filter out the ignored vulnerabilities.
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability')
UNIGNORED_VULNS=$(echo "$VULN_OUTPUT" | grep 'Vulnerability' || true)
# If the list of ignored vulnerabilities is not empty, filter them out.
if [ -n "$CLEAN_IGNORES" ]; then
UNIGNORED_VULNS=$(echo "$UNIGNORED_VULNS" | grep -vFf <(echo "$CLEAN_IGNORES") || true)
UNIGNORED_VULNS=$(echo "$UNIGNORED_VULNS" | grep -vFf <(echo "$CLEAN_IGNORES") || true)
fi
# If there are any vulnerabilities that were not in our ignore list, print them and exit with an error.
if [ -n "$UNIGNORED_VULNS" ]; then
echo "🚨 Found new, unignored vulnerabilities:"
echo "-------------------------------------"
echo "$UNIGNORED_VULNS"
echo "-------------------------------------"
echo "Exiting with an error. ❌"
exit 1
echo "🚨 Found new, unignored vulnerabilities:"
echo "-------------------------------------"
echo "$UNIGNORED_VULNS"
echo "-------------------------------------"
echo "Exiting with an error. ❌"
exit 1
else
echo "🎉 No new vulnerabilities found. All clear! ✨"
exit 0
echo "🎉 No new vulnerabilities found. All clear! ✨"
exit 0
fi
+4 -4
View File
@@ -8,7 +8,7 @@ include:
rules:
- !reference [.default-rules, run-always]
tags:
- windows-x86
- canary-windows-x86
cache: {}
##########################################
@@ -18,7 +18,7 @@ windows-build-cloudflared:
<<: *windows-build-defaults
stage: build
script:
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${GO_VERSION}" ".\.ci\scripts\windows\builds.ps1"
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\builds.ps1"
artifacts:
paths:
- artifacts/*
@@ -56,7 +56,7 @@ windows-load-env-variables:
vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/secret/key_vault_secret@kv
file: false
KEY_VAULT_CERTIFICATE:
vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/certificate_v2/key_vault_certificate@kv
vault: gitlab/cloudflare/tun/cloudflared/_dev/azure_vault/certificate/key_vault_certificate@kv
file: false
artifacts:
access: 'none'
@@ -73,7 +73,7 @@ windows-component-tests-cloudflared:
script:
# We have to decode the secret we encoded on the `windows-load-env-variables` job
- $env:COMPONENT_TESTS_ORIGINCERT = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:COMPONENT_TESTS_ORIGINCERT))
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${GO_VERSION}" ".\.ci\scripts\windows\component-test.ps1"
- powershell -ExecutionPolicy Bypass -File ".\.ci\scripts\windows\go-wrapper.ps1" "${WIN_GO_VERSION}" ".\.ci\scripts\windows\component-test.ps1"
artifacts:
reports:
junit: report.xml
+8
View File
@@ -0,0 +1,8 @@
images:
- name: cloudflared-daemon
dockerfile: Dockerfile.$ARCH
context: .
version_file: versions-internal
architectures:
- amd64
- arm64
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh
# Pre-push hook for cloudflared
# Runs linting and tests before allowing pushes
set -e
echo "========================================"
echo "Running pre-push checks..."
echo "========================================"
# Run formatting check
echo ""
echo "==> Checking formatting..."
make fmt-check
# Run linter
echo ""
echo "==> Running linter..."
make lint
# Run tests
echo ""
echo "==> Running tests..."
make test
echo ""
echo "========================================"
echo "All pre-push checks passed!"
echo "========================================"
+20 -4
View File
@@ -1,5 +1,7 @@
variables:
GO_VERSION: "go1.24.11"
GO_VERSION: "1.26.4"
MAC_GO_VERSION: "go@$GO_VERSION"
WIN_GO_VERSION: "go$GO_VERSION"
GIT_DEPTH: "0"
default:
@@ -7,7 +9,18 @@ default:
VAULT_ID_TOKEN:
aud: https://vault.cfdata.org
stages: [sync, pre-build, build, validate, test, package, release, release-internal, review]
stages:
[
sync,
pre-build,
build,
validate,
test,
package,
release,
release-internal,
review,
]
include:
#####################################################
@@ -50,9 +63,12 @@ include:
#####################################################
- local: .ci/apt-internal.gitlab-ci.yml
#####################################################
########## Release Internal Docker Image ############
#####################################################
- local: .ci/internal-image.gitlab-ci.yml
#####################################################
############## Manual Claude Review #################
#####################################################
- component: $CI_SERVER_FQDN/cloudflare/ci/ai/review@~latest
inputs:
whenToRun: "manual"
+14 -8
View File
@@ -1,3 +1,5 @@
version: "2"
linters:
enable:
# Some of the linters below are commented out. We should uncomment and start running them, but they return
@@ -14,10 +16,7 @@ linters:
- errcheck # Errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases.
- errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error.
- exhaustive # Check exhaustiveness of enum switch statements.
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification.
- goimports # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode.
- gosec # Inspects source code for security problems.
- gosimple # Linter for Go source code that specializes in simplifying code.
- govet # Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes.
- ineffassign # Detects when assignments to existing variables are not used.
- importas # Enforces consistent import aliases.
@@ -36,7 +35,13 @@ linters:
- wastedassign # Finds wasted assignment statements.
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.
- zerologlint # Detects the wrong usage of zerolog that a user forgets to dispatch with Send or Msg.
# Other linters are disabled, list of all is here: https://golangci-lint.run/usage/linters/
# Other linters are disabled, list of all is here: https://golangci-lint.run/usage/linters/
formatters:
enable:
- gofmt # Formats code according to Go standard formatting
- goimports # Formats imports and groups them properly
run:
timeout: 5m
modules-download-mode: vendor
@@ -44,9 +49,10 @@ run:
# output configuration options
output:
formats:
- format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
text:
colors: true
print-linter-name: true
print-issued-lines: true
issues:
# Maximum issues count per one linter.
@@ -67,7 +73,7 @@ issues:
new: true
# Show only new issues created after git revision `REV`.
# Default: ""
new-from-rev: ac34f94d423273c8fa8fdbb5f2ac60e55f2c77d5
new-from-rev: d2a87e9b93456ad7f82417400f4209d513668487
# Show issues in any part of update files (requires new-from-rev or new-from-patch).
# Default: false
whole-files: true
-2
View File
@@ -1,4 +1,2 @@
# Add vulnerability IDs (e.g., GO-2022-0450) to ignore, one per line.
# You can also add comments on the same line after the ID.
GO-2025-3942 # Ignore core-dns vulnerability since we will be removing the proxy-dns feature in the near future
GO-2026-4289 # Ignore core-dns vulnerability since we will be removing the proxy-dns feature in the near future
+303
View File
@@ -0,0 +1,303 @@
# Cloudflared
Cloudflare's command-line tool and networking daemon written in Go.
Production-grade tunneling and network connectivity services used by millions of
developers and organizations worldwide.
## Essential Commands
### Build & Test (Always run before commits)
```bash
# Full development check (run before any commit)
make test lint
# Build for current platform
make cloudflared
# Run all unit tests with coverage
make test
make cover
# Run specific test
go test -run TestFunctionName ./path/to/package
# Run tests with race detection
go test -race ./...
```
### Platform-Specific Builds
```bash
# Linux
TARGET_OS=linux TARGET_ARCH=amd64 make cloudflared
# Windows
TARGET_OS=windows TARGET_ARCH=amd64 make cloudflared
# macOS ARM64
TARGET_OS=darwin TARGET_ARCH=arm64 make cloudflared
# FIPS compliant build
FIPS=true make cloudflared
```
### Code Quality & Formatting
```bash
# Run linter (38+ enabled linters)
make lint
# Auto-fix formatting
make fmt
gofmt -w .
goimports -w .
# Security scanning
make vet
# Component tests (Python integration tests)
cd component-tests && python -m pytest test_file.py::test_function_name
```
Notes on linting:
- `.golangci.yaml` is configured with `new-from-rev` and `whole-files: true`.
Touching a file triggers linting of the ENTIRE file, not just the changed
hunks. Expect to fix pre-existing issues in files you modify, or add
targeted `// nolint: <linter>` comments with a short justification.
- Prefer `defer func() { _ = resource.Close() }()` over `defer resource.Close()`
for `io.Closer` values whose error truly does not matter — this satisfies
`errcheck` without hiding real failures elsewhere.
## Project Knowledge
### Package Structure
- Use meaningful package names that reflect functionality
- Package names should be lowercase, single words when possible
- Avoid generic names like `util`, `common`, `helper`
#### Well-known shared packages
- `crypto/`: Single source of truth for TLS curve preferences and other
cryptographic primitives shared by every edge-facing transport. Import as
`cfdcrypto "github.com/cloudflare/cloudflared/crypto"` to avoid colliding
with the standard library's `crypto` package. Do NOT duplicate TLS curve
or cipher selection logic in other packages.
- `tlsconfig/`: Builds the base `*tls.Config` used for edge connections
(`CreateTunnelConfig`) and loads origin/CA pools. Curve selection is
intentionally NOT set here; it is applied per-connection from the
`crypto/` package so the same config can be cloned and reused across
protocols.
- `features/`: Runtime feature flags including `PostQuantumMode`
(`PostQuantumPrefer` = default, `PostQuantumStrict` = `--post-quantum`).
- `fips/`: Build-tag driven FIPS detection. Only `fips.IsFipsEnabled()` is
exposed; never branch on `fipsEnabled` inside a function if the two
branches return the same value.
### Function and Method Guidelines
```go
// Good: Clear purpose, proper error handling
func (c *Connection) HandleRequest(ctx context.Context, req *http.Request) error {
if req == nil {
return errors.New("request cannot be nil")
}
// Implementation...
return nil
}
```
### Error Handling
- Always handle errors explicitly, never ignore them
- Use `fmt.Errorf` for error wrapping
- Create meaningful error messages with context
- Use error variables for common errors
```go
// Good error handling patterns
if err != nil {
return fmt.Errorf("failed to process connection: %w", err)
}
```
### Logging Standards
- Use `github.com/rs/zerolog` for structured logging
- Include relevant context fields
- Use appropriate log levels (Debug, Info, Warn, Error)
```go
logger.Info().
Str("tunnelID", tunnel.ID).
Int("connIndex", connIndex).
Msg("Connection established")
```
### Testing Patterns
- Use `github.com/stretchr/testify` for assertions
- Test files end with `_test.go`
- Use table-driven tests for multiple scenarios
- Always use `t.Parallel()` for parallel-safe tests
- Use meaningful test names that describe behavior
```go
func TestMetricsListenerCreation(t *testing.T) {
t.Parallel()
// Test implementation
assert.Equal(t, expected, actual)
require.NoError(t, err)
}
```
### Constants and Variables
```go
const (
MaxGracePeriod = time.Minute * 3
MaxConcurrentStreams = math.MaxUint32
LogFieldConnIndex = "connIndex"
)
var (
// Group related variables
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
flushableContentTypes = []string{sseContentType, grpcContentType, sseJsonContentType}
)
```
### Type Definitions
- Define interfaces close to their usage
- Keep interfaces small and focused
- Use descriptive names for complex types
```go
type TunnelConnection interface {
Serve(ctx context.Context) error
}
type TunnelProperties struct {
Credentials Credentials
QuickTunnelUrl string
}
```
## Key Architectural Patterns
### Context Usage
- Always accept `context.Context` as first parameter for long-running operations
- Respect context cancellation in loops and blocking operations
- Pass context through call chains
### Concurrency
- Use channels for goroutine communication
- Protect shared state with mutexes
- Prefer `sync.RWMutex` for read-heavy workloads
- `*tls.Config` values stored in shared maps (e.g.
`TunnelConfig.EdgeTLSConfigs`) must be `Clone()`d before mutating
per-connection fields like `CurvePreferences` or `NextProtos`. Writing
through the shared pointer races with concurrent connection attempts.
### TLS & Post-Quantum key exchange
- Per-connection TLS configuration for edge connections is built via
`cfdcrypto.TLSConfigWithCurvePreferences(tlsConfig, pqMode)`. It clones
the provided `*tls.Config` and sets `CurvePreferences` based on `pqMode`,
so callers never need to clone or mutate `CurvePreferences` themselves.
Do NOT reach for the package-private `getCurvePreferences` helper; the
exported `TLSConfigWithCurvePreferences` is the only supported entry
point.
- Two PQ modes are supported and apply identically to QUIC and HTTP/2:
- `PostQuantumPrefer` (default): `[X25519MLKEM768, P256Kyber768Draft00, CurveP256]`
- `PostQuantumStrict` (`--post-quantum`): `[X25519MLKEM768, P256Kyber768Draft00]`
- FIPS and non-FIPS builds use the same curve list. Do NOT reintroduce a
`fipsEnabled` branch in curve-selection code; if the two modes ever
diverge, express the divergence inside `crypto/` so call sites remain
untouched.
- HTTP/2 supports post-quantum handshakes. Never re-add a
`PostQuantumStrict`-based rejection to H2 code paths, and never force
`--post-quantum` to select QUIC-only in protocol selection.
### Configuration
- Use structured configuration with validation
- Support both file-based and CLI flag configuration
- Provide sensible defaults
### Metrics and Observability
- Instrument code with Prometheus metrics
- Use OpenTelemetry for distributed tracing
- Include structured logging with relevant context
## Boundaries
### ✅ Always Do
- Run `make test lint` before any commit
- Handle all errors explicitly with proper context
- Use `github.com/rs/zerolog` for all logging
- Add `t.Parallel()` to all parallel-safe tests
- Follow the import grouping conventions
- Use meaningful variable and function names
- Include context.Context for long-running operations
- Close resources in defer statements
### ⚠️ Ask First Before
- Adding new dependencies to go.mod
- Modifying CI/CD configuration files
- Changing build system or Makefile
- Modifying component test infrastructure
- Adding new linter rules or changing golangci-lint config
- Making breaking changes to public APIs
- Changing logging levels or structured logging fields
### 🚫 Never Do
- Ignore errors without explicit handling (`_ = err`)
- Use generic package names (`util`, `helper`, `common`)
- Commit code that fails `make test lint`
- Use `fmt.Print*` instead of structured logging
- Modify vendor dependencies directly
- Commit secrets, credentials, or sensitive data
- Use deprecated or unsafe Go patterns
- Skip testing for new functionality
- Remove existing tests unless they're genuinely invalid
## Dependencies Management
- Use Go modules (`go.mod`) exclusively
- Vendor dependencies for reproducible builds
- Keep dependencies up-to-date and secure
- Prefer standard library when possible
- Cloudflared uses a fork of quic-go always check release notes before bumping
this dependency.
## Security Considerations
- FIPS compliance support available
- Vulnerability scanning integrated in CI
- Credential handling follows security best practices
- Network security with TLS/QUIC protocols
- Regular security audits and updates
- Post quantum encryption
## Common Patterns to Follow
1. **Graceful shutdown**: Always implement proper cleanup
2. **Resource management**: Close resources in defer statements
3. **Error propagation**: Wrap errors with meaningful context
4. **Configuration validation**: Validate inputs early
5. **Logging consistency**: Use structured logging throughout
6. **Testing coverage**: Aim for comprehensive test coverage
7. **Documentation**: Comment exported functions and types
Remember: This is a mission-critical networking tool used in production by many
organizations. Code quality, security, and reliability are paramount.
+16 -1
View File
@@ -1,3 +1,18 @@
## 2026.4.0
### Breaking Change
- The default value of `--edge-ip-version` has changed from `4` to `auto`. This means cloudflared will now use whichever address family (IPv4 or IPv6) the system resolver returns first, instead of always preferring IPv4. Users who require IPv4-only connections should explicitly set `--edge-ip-version 4`.
## 2026.2.0
### Breaking Change
- Removes the `proxy-dns` feature from cloudflared. This feature allowed running a local DNS over HTTPS (DoH) proxy.
Users who relied on this functionality should migrate to alternative solutions.
Removed commands and flags:
- `cloudflared proxy-dns`
- `cloudflared tunnel proxy-dns`
- `--proxy-dns`, `--proxy-dns-port`, `--proxy-dns-address`, `--proxy-dns-upstream`, `--proxy-dns-max-upstream-conns`, `--proxy-dns-bootstrap`
- `resolver` section in configuration file
## 2025.7.1
### Notices
- `cloudflared` will no longer officially support Debian and Ubuntu distros that reached end-of-life: `buster`, `bullseye`, `impish`, `trusty`.
@@ -281,7 +296,7 @@ of uptime. Previous cloudflared versions will soon be unable to run legacy tempo
### Bug Fixes
- Tunnel create and delete commands no longer use path to credentials from the configuration file.
If you need ot place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
If you need to place tunnel credentials file at a specific location, you must use `--credentials-file` flag.
- Access ssh-gen creates properly named keys for SSH short lived certs.
+1 -1
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.24.11 AS builder
FROM golang:1.26.4 AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
+2 -2
View File
@@ -1,5 +1,5 @@
# use a builder image for building cloudflare
FROM golang:1.24.11 AS builder
FROM golang:1.26.4 AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
@@ -15,7 +15,7 @@ COPY . .
RUN GOOS=linux GOARCH=amd64 make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian13:nonroot
FROM gcr.io/distroless/base-debian13:nonroot-amd64@sha256:ced0a2b1936b14d5bddc2ee02a807b1586ca6576a967f5b043f4a3301c8a8f6b
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+2 -2
View File
@@ -1,5 +1,5 @@
# use a builder image for building cloudflare
FROM golang:1.24.11 AS builder
FROM golang:1.26.4 AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
@@ -15,7 +15,7 @@ COPY . .
RUN GOOS=linux GOARCH=arm64 make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian13:nonroot-arm64
FROM gcr.io/distroless/base-debian13:nonroot-arm64@sha256:9c1ab6a3dbf9e22827b0be4a314d7cfbe008f922b7ca833ed0e5a63318c6169e
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+10
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
@@ -289,3 +293,9 @@ ci-test: fmt-check lint test
.PHONY: ci-fips-test
ci-fips-test:
@FIPS=true $(MAKE) ci-test
.PHONY: install-hooks
install-hooks:
git config core.hooksPath .githooks
@echo "Git hooks installed from .githooks/"
@echo "Pre-push hook will run: make fmt-check lint test"
+11 -4
View File
@@ -10,7 +10,7 @@ You can also use `cloudflared` to access Tunnel origins (that are protected with
at Layer 4 (i.e., not HTTP/websocket), which is relevant for use cases such as SSH, RDP, etc.
Such usages are available under `cloudflared access help`.
You can instead use [WARP client](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/warp/)
You can instead use [WARP client](https://developers.cloudflare.com/warp-client/)
to access private origins behind Tunnels for Layer 4 traffic without requiring `cloudflared access` commands on the client side.
@@ -40,7 +40,7 @@ User documentation for Cloudflare Tunnel can be found at https://developers.clou
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels to serve traffic to your origins.
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/)
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/)
* Route traffic to that Tunnel:
* Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/dns/)
* Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/routing-to-tunnel/public-load-balancers/)
@@ -62,7 +62,7 @@ For example, as of January 2023 Cloudflare will support cloudflared version 2023
### Requirements
- [GNU Make](https://www.gnu.org/software/make/)
- [capnp](https://capnproto.org/install.html)
- [go >= 1.24](https://go.dev/doc/install)
- [go >= 1.26](https://go.dev/doc/install)
- Optional tools:
- [capnpc-go](https://pkg.go.dev/zombiezen.com/go/capnproto2/capnpc-go)
- [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports)
@@ -79,4 +79,11 @@ To locally run the tests run `make test`
To format the code and keep a good code quality use `make fmt` and `make lint`
### Mocks
After changes on interfaces you might need to regenerate the mocks, so run `make mock`
After changes on interfaces you might need to regenerate the mocks, so run `make mocks`
### Git Hooks
To avoid CI errors, you can install pre-push hooks that run linting and tests before each push:
```bash
make install-hooks
```
This will configure git to use the hooks in `.githooks/` that run `make fmt-check lint test` before each push.
+68
View File
@@ -1,3 +1,71 @@
2026.6.1
- 2026-06-18 TUN-10630: Fix precheck protocol override
- 2026-06-18 Revert "TUN-10557: Bump quic-go v0.59.1"
- 2026-06-16 chore: Fix warnings
- 2026-06-15 TUN-10612: Add renovate to cloudflared to update distroless images explicitely
- 2026-06-11 TUN-9251: Publish internal image
- 2026-05-26 TUN-10557: Bump quic-go v0.59.1
2026.6.0
- 2026-06-08 TUN-10558: Bump go to v1.24.4, x/crypto to v0.52.0 and google.golang.org/grpc to v1.81.1
- 2026-06-01 TUN-10563: introduce QUICConnection interface
2026.5.2
- 2026-05-26 TUN-10391: Avoid using fmt.Println
2026.5.1
- 2026-05-22 fix: Bump go to 1.26.3 and go.opentelemetry.io/otel and go-jose/v4 to fix CVE's
- 2026-05-22 TUN-10391: Avoid blocking cloudflared due to logging
- 2026-05-22 TUN-10391: Add precheck integration tests
- 2026-05-14 TUN-10511: Revise --edge support for pre-checks
- 2026-05-13 fix: Update golang.org/x/net to v0.54.0
- 2026-05-13 TUN-10525: Add prechecks kill switch
2026.5.0
- 2026-05-08 Bump golang.org/x/net from v0.40.0 to v0.53.0
- 2026-05-07 TUN-10507: Bump go and go-boring to 1.26.2
- 2026-05-07 TUN-10511: Add Static DNS Resolvers
- 2026-05-07 TUN-10390: Call prechecks
- 2026-05-07 TUN-10513: Disable /debug/pprof/cmdline endpoint
- 2026-05-06 TUN-10390: Fix missing TLS settings
- 2026-05-05 chore: Fix warnings
- 2026-05-04 TUN-10389: Implement main run method
- 2026-04-30 TUN-10388: Adding probe check
- 2026-04-30 TUN-10388 Implement dialers for connectivity checks
- 2026-04-30 TUN-10389: Improve probe functions
- 2026-04-29 SECENG-13496 update pkg docs for gokeyless to support multiple builds
- 2026-04-29 chore: Add pre-push hooks
- 2026-04-29 TUN-10388: Use pointer for suggested protocol
- 2026-04-27 TUN-10387: Add no-prechecks flag
- 2026-04-23 TUN-10386: Add Table Renderer
- 2026-04-21 AUTH-4699, AUTH-8460, TUN-10179: Vendor gopsutil/v4 for cross-platform process identification
- 2026-04-21 AUTH-4699, AUTH-8460, TUN-10179: Fix .lock file deletion race condition
- 2026-04-20 TUN-10413: Centralize TLS curve configuration in crypto/ and adopt X25519MLKEM768 for QUIC/H2
- 2026-04-15 TUN-10385: Add connectivity checks foundation
- 2026-04-14 chore: Fix errors in cmd
- 2026-04-14 TUN-10384: Probe TLS Helper
- 2026-04-14 TUN-10383: Set edge-ip-version to auto
- 2026-04-10 SECENG-13056 update gokeyless install instructions on pkg.cloudflare.com/index.html
- 2026-04-02 TUN-9952: Bump go to 1.26
2026.3.0
- 2026-03-05 TUN-10292: Add cloudflared management token command
- 2026-03-03 chore: Addressing small fixes and typos
- 2026-03-03 fix: Update go-sentry and go-oidc to address CVE's
- 2026-02-24 TUN-10258: add agents.md
- 2026-02-23 TUN-10267: Update mods to fix CVE GO-2026-4394
- 2026-02-20 TUN-10247: Update tail command to use /management/logs endpoint
- 2026-02-11 TUN-9858: Add more information to proxy-dns removal message
2026.2.0
- 2026-02-06 TUN-10216: TUN fix cloudflare vulnerabilities GO-2026-4340 and GO-2026-4341
- 2026-02-02 TUN-9858: Remove proxy-dns feature from cloudflared
2026.1.2
- 2026-01-23 Revert "TUN-9863: Update pipelines to use cloudflared EV Certificate"
- 2026-01-21 Revert "TUN-9886 notarize cloudflared"
- 2025-12-12 TUN-9886 notarize cloudflared
2026.1.1
- 2026-01-19 fix: Update boto3 to run on trixie
- 2026-01-19 fix: Fix wixl bundling tool for windows msi packages
+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
+27 -26
View File
@@ -2,10 +2,11 @@ package carrier
import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"math/rand"
"math/big"
"testing"
"time"
@@ -23,28 +24,19 @@ 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}
}
func TestWebsocketHeaders(t *testing.T) {
req := testRequest(t, "http://example.com", nil)
wsHeaders := websocketHeaders(req)
for _, header := range stripWebsocketHeaders {
assert.Empty(t, wsHeaders[header])
}
assert.Equal(t, "curl/7.59.0", wsHeaders.Get("User-Agent"))
}
func TestServe(t *testing.T) {
log := zerolog.Nop()
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 +48,25 @@ 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)
for range 1000 {
messageSize, err := rand.Int(rand.Reader, big.NewInt(2048))
require.NoError(t, err)
clientMessage := make([]byte, messageSize.Int64()+1)
for i := range clientMessage {
n, err := rand.Int(rand.Reader, big.NewInt(256))
n8 := uint8(n.Uint64()) //nolint:gosec // test-only
require.NoError(t, err)
clientMessage[i] = n8
}
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 +95,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]))
}
+8 -12
View File
@@ -45,9 +45,7 @@ type baseEndpoints struct {
var _ Client = (*RESTClient)(nil)
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
baseURL = strings.TrimSuffix(baseURL, "/")
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
@@ -68,7 +66,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
}
http2.ConfigureTransport(&httpTransport)
_ = http2.ConfigureTransport(&httpTransport)
return &RESTClient{
baseEndpoints: &baseEndpoints{
accountLevel: *accountLevelEndpoint,
@@ -161,7 +159,6 @@ func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T
if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
break
}
}
return fullResponse, nil
}
@@ -179,14 +176,13 @@ func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*r
}
var parsedRspBody []*T
return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
}
return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
}
type response struct {
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Errors []apiError `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Pagination Pagination `json:"result_info,omitempty"`
@@ -206,19 +202,19 @@ func (r *response) checkErrors() error {
if len(r.Errors) == 1 {
return r.Errors[0]
}
var messages string
var messagesBuilder strings.Builder
for _, e := range r.Errors {
messages += fmt.Sprintf("%s; ", e)
messagesBuilder.WriteString(fmt.Sprintf("%s; ", e))
}
return fmt.Errorf("API errors: %s", messages)
return fmt.Errorf("API errors: %s", messagesBuilder.String())
}
type apiErr struct {
type apiError struct {
Code json.Number `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func (e apiErr) Error() string {
func (e apiError) Error() string {
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
}
+1 -1
View File
@@ -8,7 +8,7 @@ type TunnelClient interface {
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
GetTunnelToken(tunnelID uuid.UUID) (string, error)
GetManagementToken(tunnelID uuid.UUID) (string, error)
GetManagementToken(tunnelID uuid.UUID, resource ManagementResource) (string, error)
DeleteTunnel(tunnelID uuid.UUID, cascade bool) error
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
+29 -11
View File
@@ -15,6 +15,27 @@ import (
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
type ManagementResource int
const (
Logs ManagementResource = iota
Admin
HostDetails
)
func (r ManagementResource) String() string {
switch r {
case Logs:
return "logs"
case Admin:
return "admin"
case HostDetails:
return "host_details"
default:
return ""
}
}
type Tunnel struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
@@ -50,10 +71,6 @@ type newTunnel struct {
TunnelSecret []byte `json:"tunnel_secret"`
}
type managementRequest struct {
Resources []string `json:"resources"`
}
type CleanupParams struct {
queryParams url.Values
}
@@ -137,15 +154,16 @@ func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error
return "", r.statusCodeToError("get tunnel token", resp)
}
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) {
// managementEndpointPath returns the path segment for a management resource endpoint
func managementEndpointPath(tunnelID uuid.UUID, res ManagementResource) string {
return fmt.Sprintf("%v/management/%s", tunnelID, res.String())
}
func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID, res ManagementResource) (token string, err error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID))
endpoint.Path = path.Join(endpoint.Path, managementEndpointPath(tunnelID, res))
body := &managementRequest{
Resources: []string{"logs"},
}
resp, err := r.sendRequest("POST", endpoint, body)
resp, err := r.sendRequest("POST", endpoint, nil)
if err != nil {
return "", errors.Wrap(err, "REST request failed")
}
+71 -7
View File
@@ -2,7 +2,6 @@ package cfapi
import (
"bytes"
"fmt"
"net"
"reflect"
"strings"
@@ -11,6 +10,7 @@ import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var loc, _ = time.LoadLocation("UTC")
@@ -52,7 +52,6 @@ func Test_unmarshalTunnel(t *testing.T) {
}
func TestUnmarshalTunnelOk(t *testing.T) {
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
expected := Tunnel{
ID: uuid.Nil,
@@ -61,12 +60,11 @@ func TestUnmarshalTunnelOk(t *testing.T) {
Connections: []Connection{},
}
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
assert.Equal(t, &expected, actual)
require.NoError(t, err)
require.Equal(t, &expected, actual)
}
func TestUnmarshalTunnelErr(t *testing.T) {
tests := []string{
`abc`,
`{"success": true, "result": abc}`,
@@ -76,7 +74,73 @@ func TestUnmarshalTunnelErr(t *testing.T) {
for i, test := range tests {
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
assert.Error(t, err, "Test #%v failed", i)
}
}
func TestManagementResource_String(t *testing.T) {
tests := []struct {
name string
resource ManagementResource
want string
}{
{
name: "Logs",
resource: Logs,
want: "logs",
},
{
name: "Admin",
resource: Admin,
want: "admin",
},
{
name: "HostDetails",
resource: HostDetails,
want: "host_details",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.resource.String())
})
}
}
func TestManagementResource_String_Unknown(t *testing.T) {
unknown := ManagementResource(999)
assert.Equal(t, "", unknown.String())
}
func TestManagementEndpointPath(t *testing.T) {
tunnelID := uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292")
tests := []struct {
name string
resource ManagementResource
want string
}{
{
name: "Logs resource",
resource: Logs,
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/logs",
},
{
name: "Admin resource",
resource: Admin,
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/admin",
},
{
name: "HostDetails resource",
resource: HostDetails,
want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/host_details",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := managementEndpointPath(tunnelID, tt.resource)
assert.Equal(t, tt.want, got)
})
}
}
@@ -97,6 +161,6 @@ func TestUnmarshalConnections(t *testing.T) {
}},
}
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, []*ActiveClient{&expected}, actual)
}
+4
View File
@@ -72,3 +72,7 @@ func (c ConnectionOptionsSnapshot) ConnectionOptions() *pogs.ConnectionOptions {
func (c ConnectionOptionsSnapshot) LogFields(event *zerolog.Event) *zerolog.Event {
return event.Strs("features", c.client.Features)
}
func (c *Config) ConnectionFeaturesSnapshot() features.FeatureSnapshot {
return c.featureSelector.Snapshot()
}
+20
View File
@@ -3,6 +3,7 @@ package access
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
@@ -23,6 +24,24 @@ func parseRequestHeaders(values []string) http.Header {
return headers
}
// bracketBareIPv6 wraps bare IPv6 addresses in a URL with square brackets.
// Go 1.26 tightened net/url parsing to strictly require RFC 3986 bracket syntax
// for IPv6 addresses in URLs. Before Go 1.26, bare forms like "http://::1" were
// accepted; now they are rejected. This function detects bare IPv6 in the host
// portion and brackets it so that url.ParseRequestURI can parse it correctly.
func bracketBareIPv6(input string) string {
prefix := input[:strings.Index(input, "://")+3]
rest := input[len(prefix):]
host := rest
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
host = rest[:i]
}
if net.ParseIP(host) != nil && strings.Contains(host, ":") {
return prefix + "[" + host + "]" + rest[len(host):]
}
return input
}
// parseHostname will attempt to convert a user provided URL string into a string with some light error checking on
// certain expectations from the URL.
// Will convert all HTTP URLs to HTTPS
@@ -33,6 +52,7 @@ func parseURL(input string) (*url.URL, error) {
if !strings.HasPrefix(input, "https://") && !strings.HasPrefix(input, "http://") {
input = fmt.Sprintf("https://%s", input)
}
input = bracketBareIPv6(input)
url, err := url.ParseRequestURI(input)
if err != nil {
return nil, fmt.Errorf("failed to parse as URL: %w", err)
+28 -3
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseRequestHeaders(t *testing.T) {
@@ -15,6 +16,30 @@ func TestParseRequestHeaders(t *testing.T) {
assert.Equal(t, "000:000:0:1:asd", values.Get("cf-trace-id"))
}
func TestBracketBareIPv6(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"https://::1", "https://[::1]"},
{"https://::1/path", "https://[::1]/path"},
{"https://::1:8080", "https://[::1:8080]"},
{"https://::1:8080/path", "https://[::1:8080]/path"},
{"https://::1?query=1", "https://[::1]?query=1"}, // query without path
{"https://::1#fragment", "https://[::1]#fragment"}, // fragment without path
{"https://[::1]", "https://[::1]"}, // already bracketed
{"https://[::1]:8080", "https://[::1]:8080"}, // already bracketed with port
{"https://127.0.0.1", "https://127.0.0.1"}, // IPv4 unchanged
{"https://example.com", "https://example.com"}, // hostname unchanged
{"https://example.com:8080", "https://example.com:8080"}, // hostname:port unchanged
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.expected, bracketBareIPv6(tt.input))
})
}
}
func TestParseURL(t *testing.T) {
schemes := []string{
"http://",
@@ -28,8 +53,8 @@ func TestParseURL(t *testing.T) {
{"localhost", "localhost"},
{"127.0.0.1", "127.0.0.1"},
{"127.0.0.1:9090", "127.0.0.1:9090"},
{"::1", "::1"},
{"::1:8080", "::1:8080"},
{"::1", "[::1]"},
{"::1:8080", "[::1:8080]"},
{"[::1]", "[::1]"},
{"[::1]:8080", "[::1]:8080"},
{":8080", ":8080"},
@@ -49,7 +74,7 @@ func TestParseURL(t *testing.T) {
input := fmt.Sprintf("%s%s%s", scheme, host.input, path)
expected := fmt.Sprintf("%s%s%s", "https://", host.expected, path)
url, err := parseURL(input)
assert.NoError(t, err, "input: %s\texpected: %s", input, expected)
require.NoError(t, err, "input: %s\texpected: %s", input, expected)
assert.Equal(t, expected, url.String())
assert.Equal(t, host.expected, url.Host)
assert.Equal(t, "https", url.Scheme)
-87
View File
@@ -1,87 +0,0 @@
package main
import (
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/tunneldns"
)
const (
// ResolverServiceType is used to identify what kind of overwatch service this is
ResolverServiceType = "resolver"
LogFieldResolverAddress = "resolverAddress"
LogFieldResolverPort = "resolverPort"
LogFieldResolverMaxUpstreamConns = "resolverMaxUpstreamConns"
)
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
// into a service model for the overwatch package.
// it also holds a reference to the config object that represents its state
type ResolverService struct {
resolver config.DNSResolver
shutdown chan struct{}
log *zerolog.Logger
}
// NewResolverService creates a new resolver service
func NewResolverService(r config.DNSResolver, log *zerolog.Logger) *ResolverService {
return &ResolverService{resolver: r,
shutdown: make(chan struct{}),
log: log,
}
}
// Name is used to figure out this service is related to the others (normally the addr it binds to)
// this is just "resolver" since there can only be one DNS resolver running
func (s *ResolverService) Name() string {
return ResolverServiceType
}
// Type is used to identify what kind of overwatch service this is
func (s *ResolverService) Type() string {
return ResolverServiceType
}
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
func (s *ResolverService) Hash() string {
return s.resolver.Hash()
}
// Shutdown stops the tunneldns listener
func (s *ResolverService) Shutdown() {
s.shutdown <- struct{}{}
}
// Run is the run loop that is started by the overwatch service
func (s *ResolverService) Run() error {
// create a listener
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.resolver.MaxUpstreamConnectionsOrDefault(), s.log)
if err != nil {
return err
}
// start the listener.
readySignal := make(chan struct{})
err = l.Start(readySignal)
if err != nil {
_ = l.Stop()
return err
}
<-readySignal
resolverLog := s.log.With().
Str(LogFieldResolverAddress, s.resolver.AddressOrDefault()).
Uint16(LogFieldResolverPort, s.resolver.PortOrDefault()).
Int(LogFieldResolverMaxUpstreamConns, s.resolver.MaxUpstreamConnectionsOrDefault()).
Logger()
resolverLog.Info().Msg("Starting resolver")
// wait for shutdown signal
<-s.shutdown
resolverLog.Info().Msg("Shutting down resolver")
return l.Stop()
}
+1 -9
View File
@@ -8,7 +8,7 @@ import (
)
// AppService is the main service that runs when no command lines flags are passed to cloudflared
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
// it manages all the running services such as tunnels, forwarders, etc
type AppService struct {
configManager config.Manager
serviceManager overwatch.Manager
@@ -73,14 +73,6 @@ func (s *AppService) handleConfigUpdate(c config.Root) {
activeServices[service.Name()] = struct{}{}
}
// handle resolver changes
if c.Resolver.Enabled {
service := NewResolverService(c.Resolver, s.log)
s.serviceManager.Add(service)
activeServices[service.Name()] = struct{}{}
}
// TODO: TUN-1451 - tunnels
// remove any services that are no longer active
+57
View File
@@ -1,6 +1,9 @@
package cliutil
import (
"strings"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
FlagLogOutput,
}
}
// LogTable renders lines inside an ASCII table and logs each rendered row.
func LogTable(log *zerolog.Logger, lines []string, title ...string) {
tableTitle := ""
if len(title) > 0 {
tableTitle = title[0]
}
for _, line := range asciiBox(lines, tableTitle, 2) {
if line != "" {
log.Info().Msg(line)
}
}
}
// asciiBox wraps lines in a bordered ASCII box with an optional title row.
func asciiBox(lines []string, title string, padding int) (box []string) {
maxLen := maxLen(lines, title)
spacer := strings.Repeat(" ", padding)
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
box = append(box, border)
if title != "" {
box = append(box, renderBoxLine(centerLine(title, maxLen), maxLen, spacer))
box = append(box, border)
}
for _, line := range lines {
box = append(box, renderBoxLine(line, maxLen, spacer))
}
box = append(box, border)
return
}
// renderBoxLine pads a single line so it fills the box width.
func renderBoxLine(line string, maxLen int, spacer string) string {
return "|" + spacer + line + strings.Repeat(" ", maxLen-len(line)) + spacer + "|"
}
// centerLine pads line evenly so it is centered within width.
func centerLine(line string, width int) string {
padding := width - len(line)
leftPadding := padding / 2
rightPadding := padding - leftPadding
return strings.Repeat(" ", leftPadding) + line + strings.Repeat(" ", rightPadding)
}
// maxLen returns the longest visible line length including the title.
func maxLen(lines []string, title string) int {
max := len(title)
for _, line := range lines {
if len(line) > max {
max = len(line)
}
}
return max
}
+60
View File
@@ -0,0 +1,60 @@
package cliutil
import (
"bytes"
"encoding/json"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLogTableWithoutTitle(t *testing.T) {
t.Parallel()
lines := captureTableLogs(t, []string{"first", "second"})
assert.Equal(t, []string{
"+----------+",
"| first |",
"| second |",
"+----------+",
}, lines)
}
func TestLogTableWithTitle(t *testing.T) {
t.Parallel()
lines := captureTableLogs(t, []string{"first", "second"}, "TT")
assert.Equal(t, []string{
"+----------+",
"| TT |",
"+----------+",
"| first |",
"| second |",
"+----------+",
}, lines)
}
func captureTableLogs(t *testing.T, lines []string, title ...string) []string {
t.Helper()
var buf bytes.Buffer
logger := zerolog.New(&buf)
LogTable(&logger, lines, title...)
// nolint: prealloc
var messages []string
for _, line := range bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n")) {
var entry struct {
Message string `json:"message"`
}
require.NoError(t, json.Unmarshal(line, &entry))
messages = append(messages, entry.Message)
}
return messages
}
+84
View File
@@ -0,0 +1,84 @@
package cliutil
import (
"errors"
"fmt"
"io"
"os"
"time"
"github.com/google/uuid"
"github.com/mattn/go-colorable"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials"
)
// Error definitions for management token operations
var (
ErrNoTunnelID = errors.New("no tunnel ID provided")
ErrInvalidTunnelID = errors.New("unable to parse provided tunnel id as a valid UUID")
)
// GetManagementToken acquires a management token from Cloudflare API for the specified resource
func GetManagementToken(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource, buildInfo *BuildInfo) (string, error) {
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
if err != nil {
return "", err
}
var apiURL string
if userCreds.IsFEDEndpoint() {
apiURL = credentials.FedRampBaseApiURL
} else {
apiURL = c.String(cfdflags.ApiURL)
}
client, err := userCreds.Client(apiURL, buildInfo.UserAgent(), log)
if err != nil {
return "", err
}
tunnelIDString := c.Args().First()
if tunnelIDString == "" {
return "", ErrNoTunnelID
}
tunnelID, err := uuid.Parse(tunnelIDString)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrInvalidTunnelID, err)
}
token, err := client.GetManagementToken(tunnelID, res)
if err != nil {
return "", err
}
return token, nil
}
// CreateStderrLogger creates a logger that outputs to stderr to avoid interfering with stdout
func CreateStderrLogger(c *cli.Context) *zerolog.Logger {
level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel))
if levelErr != nil {
level = zerolog.InfoLevel
}
var writer io.Writer
switch c.String(cfdflags.LogFormatOutput) {
case cfdflags.LogFormatOutputValueJSON:
// zerolog by default outputs as JSON
writer = os.Stderr
case cfdflags.LogFormatOutputValueDefault:
// "default" and unset use the same logger output format
fallthrough
default:
writer = zerolog.ConsoleWriter{
Out: colorable.NewColorable(os.Stderr),
TimeFormat: time.RFC3339,
}
}
log := zerolog.New(writer).With().Timestamp().Logger().Level(level)
return &log
}
+6 -3
View File
@@ -81,6 +81,9 @@ const (
// EdgeBindAddress is the command line flag to bind to IP address for outgoing connections to Cloudflare Edge
EdgeBindAddress = "edge-bind-address"
// CACert Certificate Authority authenticating connections with Cloudflare's edge network.
CACert = "cacert"
// Force is the command line flag to specify if you wish to force an action
Force = "force"
@@ -111,9 +114,6 @@ const (
// ICMPV6Src is the command line flag to set the source address and the interface name to send/receive ICMPv6 messages
ICMPV6Src = "icmpv6-src"
// ProxyDns is the command line flag to run DNS server over HTTPS
ProxyDns = "proxy-dns"
// Name is the command line to set the name of the tunnel
Name = "name"
@@ -123,6 +123,9 @@ const (
// NoAutoUpdate is the command line flag to disable cloudflared from checking for updates
NoAutoUpdate = "no-autoupdate"
// NoPrechecks is the command line flag to skip connectivity pre-checks at startup.
NoPrechecks = "no-prechecks"
// LogLevel is the command line flag for the cloudflared logging level
LogLevel = "loglevel"
+4 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/cmd/cloudflared/management"
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tail"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
@@ -91,6 +92,7 @@ func main() {
tracing.Init(Version)
token.Init(Version)
tail.Init(bInfo)
management.Init(bInfo)
runApp(app, graceShutdownC)
}
@@ -149,9 +151,10 @@ To determine if an update happened in a script, check for error code 11.`,
},
}
cmds = append(cmds, tunnel.Commands()...)
cmds = append(cmds, proxydns.Command(false))
cmds = append(cmds, proxydns.Command()) // removed feature, only here for error message
cmds = append(cmds, access.Commands()...)
cmds = append(cmds, tail.Command())
cmds = append(cmds, management.Command())
return cmds
}
+105
View File
@@ -0,0 +1,105 @@
package management
import (
"encoding/json"
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials"
)
var buildInfo *cliutil.BuildInfo
// Init initializes the management package with build info
func Init(bi *cliutil.BuildInfo) {
buildInfo = bi
}
// Command returns the management command with its subcommands
func Command() *cli.Command {
return &cli.Command{
Name: "management",
Usage: "Monitor cloudflared tunnels via management API",
Category: "Management",
Hidden: true,
Subcommands: []*cli.Command{
buildTokenSubcommand(),
},
}
}
// buildTokenSubcommand creates the token subcommand
func buildTokenSubcommand() *cli.Command {
return &cli.Command{
Name: "token",
Action: cliutil.ConfiguredAction(tokenCommand),
Usage: "Get management access jwt for a specific resource",
UsageText: "cloudflared management token --resource <resource> TUNNEL_ID",
Description: "Get management access jwt for a tunnel with specified resource permissions (logs, admin, host_details)",
Hidden: true,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "resource",
Usage: "Resource type for token permissions: logs, admin, or host_details",
Required: true,
},
&cli.StringFlag{
Name: cfdflags.OriginCert,
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
Value: credentials.FindDefaultOriginCertPath(),
},
&cli.StringFlag{
Name: cfdflags.LogLevel,
Value: "info",
Usage: "Application logging level {debug, info, warn, error, fatal}",
EnvVars: []string{"TUNNEL_LOGLEVEL"},
},
cliutil.FlagLogOutput,
},
}
}
// tokenCommand handles the token subcommand execution
func tokenCommand(c *cli.Context) error {
log := cliutil.CreateStderrLogger(c)
// Parse and validate resource flag
resourceStr := c.String("resource")
resource, err := parseResource(resourceStr)
if err != nil {
return fmt.Errorf("invalid resource '%s': %w", resourceStr, err)
}
// Get management token
token, err := cliutil.GetManagementToken(c, log, resource, buildInfo)
if err != nil {
return err
}
// Output JSON to stdout
tokenResponse := struct {
Token string `json:"token"`
}{Token: token}
return json.NewEncoder(os.Stdout).Encode(tokenResponse)
}
// parseResource converts resource string to ManagementResource enum
func parseResource(resource string) (cfapi.ManagementResource, error) {
switch resource {
case "logs":
return cfapi.Logs, nil
case "admin":
return cfapi.Admin, nil
case "host_details":
return cfapi.HostDetails, nil
default:
return 0, fmt.Errorf("must be one of: logs, admin, host_details")
}
}
+71
View File
@@ -0,0 +1,71 @@
package management
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/cfapi"
)
func TestParseResource_ValidResources(t *testing.T) {
t.Parallel()
tests := []struct {
input string
expected cfapi.ManagementResource
}{
{"logs", cfapi.Logs},
{"admin", cfapi.Admin},
{"host_details", cfapi.HostDetails},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
result, err := parseResource(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseResource_InvalidResource(t *testing.T) {
t.Parallel()
invalid := []string{"invalid", "LOGS", "Admin", "", "metrics", "host-details"}
for _, input := range invalid {
t.Run(input, func(t *testing.T) {
t.Parallel()
_, err := parseResource(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "must be one of")
})
}
}
func TestCommandStructure(t *testing.T) {
t.Parallel()
cmd := Command()
assert.Equal(t, "management", cmd.Name)
assert.True(t, cmd.Hidden)
assert.Len(t, cmd.Subcommands, 1)
tokenCmd := cmd.Subcommands[0]
assert.Equal(t, "token", tokenCmd.Name)
assert.True(t, tokenCmd.Hidden)
// Verify required flags exist
var hasResourceFlag bool
for _, flag := range tokenCmd.Flags {
if flag.Names()[0] == "resource" {
hasResourceFlag = true
break
}
}
assert.True(t, hasResourceFlag, "token command should have --resource flag")
}
+35 -96
View File
@@ -1,115 +1,54 @@
package proxydns
import (
"context"
"net"
"os"
"os/signal"
"syscall"
"errors"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/tunneldns"
)
func Command(hidden bool) *cli.Command {
return &cli.Command{
Name: "proxy-dns",
Action: cliutil.ConfiguredAction(Run),
const removedMessage = "dns-proxy feature is no longer supported"
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
},
&cli.StringFlag{
Name: "address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
},
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
&cli.IntFlag{
Name: "port",
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
Value: 53,
EnvVars: []string{"TUNNEL_DNS_PORT"},
},
&cli.StringSliceFlag{
Name: "upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
&cli.StringSliceFlag{
Name: "bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
},
&cli.IntFlag{
Name: "max-upstream-conns",
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
Value: tunneldns.MaxUpstreamConnsDefault,
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: hidden,
func Command() *cli.Command {
return &cli.Command{
Name: "proxy-dns",
Action: cliutil.ConfiguredAction(Run),
Usage: removedMessage,
SkipFlagParsing: true,
}
}
// Run implements a foreground runner
func Run(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
err := errors.New(removedMessage)
log.Error().Msg("DNS Proxy is no longer supported since version 2026.2.0 (https://developers.cloudflare.com/changelog/2025-11-11-cloudflared-proxy-dns/). As an alternative consider using https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/dns-over-https-client/")
metricsListener, err := net.Listen("tcp", c.String("metrics"))
if err != nil {
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
}
go metrics.ServeMetrics(metricsListener, context.Background(), metrics.Config{}, log)
listener, err := tunneldns.CreateListener(
c.String("address"),
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
uint16(c.Int("port")),
c.StringSlice("upstream"),
c.StringSlice("bootstrap"),
c.Int("max-upstream-conns"),
log,
)
if err != nil {
log.Err(err).Msg("Failed to create the listeners")
return err
}
// Try to start the server
readySignal := make(chan struct{})
err = listener.Start(readySignal)
if err != nil {
log.Err(err).Msg("Failed to start the listeners")
return listener.Stop()
}
<-readySignal
// Wait for signal
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(signals)
<-signals
// Shut down server
err = listener.Stop()
if err != nil {
log.Err(err).Msg("failed to stop")
}
return err
}
// Old flags used by the proxy-dns command, only kept to not break any script that might be setting these flags
func ConfigureProxyDNSFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-dns",
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-port",
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proxy-dns-address",
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-upstream",
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-max-upstream-conns",
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-bootstrap",
}),
}
}
+7 -69
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
@@ -13,11 +12,11 @@ import (
"time"
"github.com/google/uuid"
"github.com/mattn/go-colorable"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"nhooyr.io/websocket"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials"
@@ -50,9 +49,9 @@ func buildTailManagementTokenSubcommand() *cli.Command {
}
func managementTokenCommand(c *cli.Context) error {
log := createLogger(c)
log := cliutil.CreateStderrLogger(c)
token, err := getManagementToken(c, log)
token, err := cliutil.GetManagementToken(c, log, cfapi.Logs, buildInfo)
if err != nil {
return err
}
@@ -161,31 +160,6 @@ func handleValidationError(resp *http.Response, log *zerolog.Logger) {
}
}
// logger will be created to emit only against the os.Stderr as to not obstruct with normal output from
// management requests
func createLogger(c *cli.Context) *zerolog.Logger {
level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel))
if levelErr != nil {
level = zerolog.InfoLevel
}
var writer io.Writer
switch c.String(cfdflags.LogFormatOutput) {
case cfdflags.LogFormatOutputValueJSON:
// zerolog by default outputs as JSON
writer = os.Stderr
case cfdflags.LogFormatOutputValueDefault:
// "default" and unset use the same logger output format
fallthrough
default:
writer = zerolog.ConsoleWriter{
Out: colorable.NewColorable(os.Stderr),
TimeFormat: time.RFC3339,
}
}
log := zerolog.New(writer).With().Timestamp().Logger().Level(level)
return &log
}
// parseFilters will attempt to parse provided filters to send to with the EventStartStreaming
func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
var level *management.LogLevel
@@ -230,49 +204,13 @@ func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
}, nil
}
// getManagementToken will make a call to the Cloudflare API to acquire a management token for the requested tunnel.
func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
if err != nil {
return "", err
}
var apiURL string
if userCreds.IsFEDEndpoint() {
apiURL = credentials.FedRampBaseApiURL
} else {
apiURL = c.String(cfdflags.ApiURL)
}
client, err := userCreds.Client(apiURL, buildInfo.UserAgent(), log)
if err != nil {
return "", err
}
tunnelIDString := c.Args().First()
if tunnelIDString == "" {
return "", errors.New("no tunnel ID provided")
}
tunnelID, err := uuid.Parse(tunnelIDString)
if err != nil {
return "", errors.New("unable to parse provided tunnel id as a valid UUID")
}
token, err := client.GetManagementToken(tunnelID)
if err != nil {
return "", err
}
return token, nil
}
// buildURL will build the management url to contain the required query parameters to authenticate the request.
func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
func buildURL(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource) (url.URL, error) {
var err error
token := c.String("token")
if token == "" {
token, err = getManagementToken(c, log)
token, err = cliutil.GetManagementToken(c, log, res, buildInfo)
if err != nil {
return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err)
}
@@ -323,7 +261,7 @@ func printJSON(log *management.Log, logger *zerolog.Logger) {
// Run implements a foreground runner
func Run(c *cli.Context) error {
log := createLogger(c)
log := cliutil.CreateStderrLogger(c)
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
@@ -345,7 +283,7 @@ func Run(c *cli.Context) error {
return nil
}
u, err := buildURL(c, log)
u, err := buildURL(c, log, cfapi.Logs)
if err != nil {
log.Err(err).Msg("unable to construct management request URL")
return nil
+84 -110
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"net"
"net/url"
"os"
"path/filepath"
@@ -31,20 +32,22 @@ import (
"github.com/cloudflare/cloudflared/credentials"
"github.com/cloudflare/cloudflared/diagnostic"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/management"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/orchestration"
"github.com/cloudflare/cloudflared/prechecks"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstate"
"github.com/cloudflare/cloudflared/validation"
)
const (
//nolint:gosec // This is the Sentry DSN for cloudflared which is safe to be public
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
LogFieldCommand = "command"
@@ -77,6 +80,7 @@ var (
"config",
cfdflags.AutoUpdateFreq,
cfdflags.NoAutoUpdate,
cfdflags.NoPrechecks,
cfdflags.Metrics,
"pidfile",
"url",
@@ -115,12 +119,6 @@ var (
cfdflags.LogFile,
cfdflags.LogDirectory,
cfdflags.TraceOutput,
cfdflags.ProxyDns,
"proxy-dns-port",
"proxy-dns-address",
"proxy-dns-upstream",
"proxy-dns-max-upstream-conns",
"proxy-dns-bootstrap",
cfdflags.IsAutoUpdated,
cfdflags.Edge,
cfdflags.Region,
@@ -181,8 +179,7 @@ func Commands() []*cli.Command {
buildCleanupCommand(),
buildTokenCommand(),
buildDiagCommand(),
// for compatibility, allow following as tunnel subcommands
proxydns.Command(true),
proxydns.Command(), // removed feature, only here for error message
cliutil.RemovedCommand("db-connect"),
}
@@ -238,7 +235,7 @@ func TunnelCommand(c *cli.Context) error {
return err
}
// Run a adhoc named tunnel
// Run an adhoc named tunnel
// Allows for the creation, routing (optional), and startup of a tunnel in one command
// --name required
// --url or --hello-world required
@@ -248,8 +245,8 @@ func TunnelCommand(c *cli.Context) error {
if err != nil {
return errors.Wrap(err, "Invalid hostname provided")
}
url := c.String("url")
if url == hostname && url != "" && hostname != "" {
tunnelURL := c.String("url")
if tunnelURL == hostname && tunnelURL != "" && hostname != "" {
return fmt.Errorf("hostname and url shouldn't match. See --help for more information")
}
@@ -258,15 +255,14 @@ func TunnelCommand(c *cli.Context) error {
// Run a quick tunnel
// A unauthenticated named tunnel hosted on <random>.<quick-tunnels-service>.com
// We don't support running proxy-dns and a quick tunnel at the same time as the same process
shouldRunQuickTunnel := c.IsSet("url") || c.IsSet(ingress.HelloWorldFlag)
if !c.IsSet(cfdflags.ProxyDns) && c.String("quick-service") != "" && shouldRunQuickTunnel {
if c.String("quick-service") != "" && shouldRunQuickTunnel {
return RunQuickTunnel(sc)
}
// If user provides a config, check to see if they meant to use `tunnel run` instead
if ref := config.GetConfiguration().TunnelID; ref != "" {
return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
return fmt.Errorf("use `cloudflared tunnel run` to start tunnel %s", ref)
}
// Classic tunnel usage is no longer supported
@@ -274,16 +270,6 @@ func TunnelCommand(c *cli.Context) error {
return errDeprecatedClassicTunnel
}
if c.IsSet(cfdflags.ProxyDns) {
if shouldRunQuickTunnel {
return fmt.Errorf("running a quick tunnel with `proxy-dns` is not supported")
}
// NamedTunnelProperties are nil since proxy dns server does not need it.
// This is supported for legacy reasons: dns proxy server is not a tunnel and ideally should
// not run as part of cloudflared tunnel.
return StartServer(sc.c, buildInfo, nil, sc.log)
}
return errors.New(tunnelCmdErrorMessage)
}
@@ -364,12 +350,14 @@ func StartServer(
traceLog.Err(err).Msg("Failed to close temporary trace output file")
}
traceOutputFilepath := c.String(cfdflags.TraceOutput)
//nolint:gosec // File path is safe because it is explicitly provided by the user via the --trace-output flag
if err := os.Rename(tmpTraceFile.Name(), traceOutputFilepath); err != nil {
traceLog.
Err(err).
Str(LogFieldTraceOutputFilepath, traceOutputFilepath).
Msg("Failed to rename temporary trace output file")
} else {
//nolint:gosec // File path is safe, since it is created by os.CreateTemp
err := os.Remove(tmpTraceFile.Name())
if err != nil {
traceLog.Err(err).Msg("Failed to remove the temporary trace file")
@@ -387,30 +375,18 @@ func StartServer(
info.Log(log)
logClientOptions(c, log)
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
// this context drives the server, when it's canceled tunnel and all other components (origins, dns, etc...) should stop
ctx, cancel := context.WithCancel(c.Context)
defer cancel()
go waitForSignal(graceShutdownC, log)
if c.IsSet(cfdflags.ProxyDns) {
dnsReadySignal := make(chan struct{})
wg.Add(1)
go func() {
defer wg.Done()
errC <- runDNSProxyServer(c, dnsReadySignal, ctx.Done(), log)
}()
// Wait for proxy-dns to come up (if used)
<-dnsReadySignal
}
connectedSignal := signal.New(make(chan struct{}))
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"), log)
}
// update needs to be after DNS proxy is up to resolve equinox server address
wg.Add(1)
go func() {
defer wg.Done()
@@ -420,15 +396,8 @@ func StartServer(
errC <- autoupdater.Run(ctx)
}()
// Serve DNS proxy stand-alone if no tunnel type (quick, adhoc, named) is going to run
if dnsProxyStandAlone(c, namedTunnel) {
connectedSignal.Notify()
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, log)
}
if namedTunnel == nil {
return fmt.Errorf("namedTunnel is nil outside of DNS proxy stand-alone mode")
return fmt.Errorf("namedTunnel is nil")
}
logTransport := logger.CreateTransportLoggerFromContext(c, logger.EnableTerminalLog)
@@ -448,6 +417,13 @@ func StartServer(
}
connectorID := tunnelConfig.ClientConfig.ConnectorID
// Run connectivity pre-checks for cloudflared. This runs in a separate
// goroutine, as we want to keep initializing cloudflared while prechecks
// are running. Prechecks are controlled via DNS flag for remote kill-switch capability.
if !tunnelConfig.ClientConfig.ConnectionFeaturesSnapshot().SkipPrechecks && !c.Bool(cfdflags.NoPrechecks) {
go runPrechecks(c, log, tunnelConfig.Region)
}
// Disable ICMP packet routing for quick tunnels
if quickTunnelURL != "" {
tunnelConfig.ICMPRouterServer = nil
@@ -489,7 +465,7 @@ func StartServer(
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
defer func() { _ = metricsListener.Close() }()
wg.Add(1)
go func() {
@@ -547,6 +523,42 @@ func StartServer(
return waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, log)
}
// 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) {
ipVersion := allregions.Auto
if ipVersionStr := c.String(cfdflags.EdgeIpVersion); ipVersionStr != "" {
parsedVersion, err := parseConfigIPVersion(ipVersionStr)
if err == nil {
ipVersion = parsedVersion
} else {
log.Warn().Str("edgeIpVersion", ipVersionStr).Err(err).Msg("Invalid edge-ip-version value, using auto")
}
}
cfg := prechecks.Config{
Region: region,
IPVersion: ipVersion,
EdgeAddrs: c.StringSlice(cfdflags.Edge),
ProtocolOverride: c.String(cfdflags.Protocol),
}
dialers := prechecks.RunDialers{
DNSResolver: &prechecks.EdgeDNSResolver{Log: log},
TCPDialer: &prechecks.EdgeTCPDialer{},
QUICDialer: &prechecks.EdgeQUICDialer{},
ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}},
}
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
// Output the human-readable table
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
// Also log structured results for log aggregation
report.LogEvent(log)
}
func waitToShutdown(wg *sync.WaitGroup,
cancelServerContext func(),
errC <-chan error,
@@ -603,13 +615,14 @@ func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog
log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile")
return
}
file, err := os.Create(expandedPath)
cleanPath := filepath.Clean(expandedPath)
file, err := os.Create(cleanPath)
if err != nil {
log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid")
return
}
defer file.Close()
fmt.Fprintf(file, "%d", os.Getpid())
defer func() { _ = file.Close() }()
_, _ = fmt.Fprintf(file, "%d", os.Getpid())
}
func hostnameFromURI(uri string) string {
@@ -641,7 +654,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
flags := configureCloudflaredFlags(shouldHide)
flags = append(flags, configureProxyFlags(shouldHide)...)
flags = append(flags, cliutil.ConfigureLoggingFlags(shouldHide)...)
flags = append(flags, configureProxyDNSFlags(shouldHide)...)
flags = append(flags, proxydns.ConfigureProxyDNSFlags(shouldHide)...) // removed feature, only kept to not break any script that might be setting these flags
flags = append(flags, []cli.Flag{
credentialsFileFlag,
altsrc.NewBoolFlag(&cli.BoolFlag{
@@ -665,7 +678,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Name: cfdflags.EdgeIpVersion,
Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}",
EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"},
Value: "4",
Value: "auto",
Hidden: false,
}),
altsrc.NewStringFlag(&cli.StringFlag{
@@ -675,7 +688,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: false,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: tlsconfig.CaCertFlag,
Name: cfdflags.CACert,
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
EnvVars: []string{"TUNNEL_CACERT"},
Hidden: true,
@@ -915,6 +928,13 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
Value: false,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: cfdflags.NoPrechecks,
Usage: "Skip connectivity pre-checks at startup.",
EnvVars: []string{"TUNNEL_NO_PRECHECKS"},
Value: false,
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: cfdflags.Metrics,
Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
@@ -938,6 +958,7 @@ and virtualized host network stacks from each other`,
}
func configureProxyFlags(shouldHide bool) []cli.Flag {
//nolint: prealloc
flags := []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{
Name: "url",
@@ -1172,58 +1193,13 @@ func sshFlags(shouldHide bool) []cli.Flag {
}
}
func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: cfdflags.ProxyDns,
Usage: "Run a DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-port",
Value: 53,
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS_PORT"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proxy-dns-address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
Hidden: shouldHide,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-max-upstream-conns",
Usage: "Maximum concurrent connections to upstream. Setting to 0 means unlimited.",
Value: tunneldns.MaxUpstreamConnsDefault,
Hidden: shouldHide,
EnvVars: []string{"TUNNEL_DNS_MAX_UPSTREAM_CONNS"},
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice(
"https://162.159.36.1/dns-query",
"https://162.159.46.1/dns-query",
"https://[2606:4700:4700::1111]/dns-query",
"https://[2606:4700:4700::1001]/dns-query",
),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
Hidden: shouldHide,
}),
}
}
func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logger) {
helpStr := strings.Join([]string{
"Supported command:",
"reconnect [delay]",
"- restarts one randomly chosen connection with optional delay before reconnect\n",
}, "\n")
for {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
@@ -1232,7 +1208,7 @@ func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logg
switch parts[0] {
case "":
break
continue
case "reconnect":
var reconnect supervisor.ReconnectSignal
if len(parts) > 1 {
@@ -1244,13 +1220,11 @@ func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logg
}
log.Info().Msgf("Sending %+v", reconnect)
reconnectCh <- reconnect
case "help":
log.Info().Msg(helpStr)
default:
log.Info().Str(LogFieldCommand, command).Msg("Unknown command")
fallthrough
case "help":
log.Info().Msg(`Supported command:
reconnect [delay]
- restarts one randomly chosen connection with optional delay before reconnect`)
log.Info().Msg(helpStr)
}
}
}
+6 -22
View File
@@ -111,13 +111,6 @@ func isSecretEnvVar(key string) bool {
return false
}
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.TunnelProperties) bool {
return c.IsSet(flags.ProxyDns) &&
!(c.IsSet(flags.Name) || // adhoc-named tunnel
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
namedTunnel != nil) // named tunnel
}
func prepareTunnelConfig(
ctx context.Context,
c *cli.Context,
@@ -147,23 +140,13 @@ func prepareTunnelConfig(
}
tags = append(tags, pogs.Tag{Name: "ID", Value: clientConfig.ConnectorID.String()})
clientFeatures := featureSelector.Snapshot()
pqMode := clientFeatures.PostQuantum
if pqMode == features.PostQuantumStrict {
// Error if the user tries to force a non-quic transport protocol
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
}
transportProtocol = connection.QUIC.String()
}
cfg := config.GetConfiguration()
ingressRules, err := ingress.ParseIngressFromConfigAndCLI(cfg, c, log)
if err != nil {
return nil, nil, err
}
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), isPostQuantumEnforced, edgediscovery.ProtocolPercentage, connection.ResolveTTL, log)
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), edgediscovery.ProtocolPercentage, connection.ResolveTTL, log)
if err != nil {
return nil, nil, err
}
@@ -175,7 +158,7 @@ func prepareTunnelConfig(
if tlsSettings == nil {
return nil, nil, fmt.Errorf("%s has unknown TLS settings", p)
}
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName)
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c.String(flags.CACert), tlsSettings.ServerName)
if err != nil {
return nil, nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
@@ -268,6 +251,7 @@ func prepareTunnelConfig(
DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery),
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
NoPrechecks: c.Bool(flags.NoPrechecks),
OriginDNSService: dnsService,
OriginDialerService: originDialerService,
}
@@ -307,7 +291,7 @@ func gracePeriod(c *cli.Context) (time.Duration, error) {
}
func isRunningFromTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd()))
return term.IsTerminal(int(os.Stdout.Fd())) // nolint:gosec
}
// ParseConfigIPVersion returns the IP version from possible expected values from config
@@ -348,7 +332,7 @@ func testIPBindable(ip net.IP) error {
if err != nil {
return err
}
listener.Close()
_ = listener.Close()
return nil
}
@@ -510,7 +494,7 @@ func findLocalAddr(dst net.IP, port int) (netip.Addr, error) {
if err != nil {
return netip.Addr{}, err
}
defer udpConn.Close()
defer func() { _ = udpConn.Close() }()
localAddrPort, err := netip.ParseAddrPort(udpConn.LocalAddr().String())
if err != nil {
return netip.Addr{}, err
+2 -1
View File
@@ -100,6 +100,7 @@ func login(c *cli.Context) error {
c.Bool(cfdflags.AutoCloseInterstitial),
isFEDRamp,
log,
"",
)
if err != nil {
log.Error().Err(err).Msgf("Failed to write the certificate.\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", path)
@@ -122,7 +123,7 @@ func login(c *cli.Context) error {
return err
}
if err := os.WriteFile(path, resourceData, 0600); err != nil {
if err := os.WriteFile(path, resourceData, 0600); err != nil { // nolint: gosec
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
}
+4 -28
View File
@@ -11,6 +11,7 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/connection"
)
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
if err != nil {
return errors.Wrap(err, "failed to request quick Tunnel")
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
// This will read the entire response into memory so we can print it in case of error
rsp_body, err := io.ReadAll(resp.Body)
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
url = "https://" + url
}
for _, line := range AsciiBox([]string{
cliutil.LogTable(sc.log, []string{
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
url,
}, 2) {
sc.log.Info().Msg(line)
}
})
if !sc.c.IsSet(flags.Protocol) {
_ = sc.c.Set(flags.Protocol, "quic")
@@ -116,26 +115,3 @@ type QuickTunnel struct {
AccountTag string `json:"account_tag"`
Secret []byte `json:"secret"`
}
// Print out the given lines in a nice ASCII box.
func AsciiBox(lines []string, padding int) (box []string) {
maxLen := maxLen(lines)
spacer := strings.Repeat(" ", padding)
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
box = append(box, border)
for _, line := range lines {
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
}
box = append(box, border)
return
}
func maxLen(lines []string) int {
max := 0
for _, line := range lines {
if len(line) > max {
max = len(line)
}
}
return max
}
-37
View File
@@ -1,37 +0,0 @@
package tunnel
import (
"fmt"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
)
func runDNSProxyServer(c *cli.Context, dnsReadySignal chan struct{}, shutdownC <-chan struct{}, log *zerolog.Logger) error {
port := c.Int("proxy-dns-port")
if port <= 0 || port > 65535 {
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
}
maxUpstreamConnections := c.Int("proxy-dns-max-upstream-conns")
if maxUpstreamConnections < 0 {
return fmt.Errorf("'%s' must be 0 or higher", "proxy-dns-max-upstream-conns")
}
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), maxUpstreamConnections, log)
if err != nil {
close(dnsReadySignal)
listener.Stop()
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
}
err = listener.Start(dnsReadySignal)
if err != nil {
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
}
<-shutdownC
_ = listener.Stop()
log.Info().Msg("DNS server stopped")
return nil
}
+20 -15
View File
@@ -421,7 +421,7 @@ func listCommand(c *cli.Context) error {
func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) {
writer := tabWriter()
defer writer.Flush()
defer func() { _ = writer.Flush() }()
_, _ = fmt.Fprintln(writer, "You can obtain more detailed information for each tunnel with `cloudflared tunnel info <name/uuid>`")
@@ -444,13 +444,14 @@ func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
// Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections))
for _, connection := range connections {
if !connection.IsPendingReconnect || showRecentlyDisconnected {
numConnsPerColo[connection.ColoName]++
for _, cfConnections := range connections {
if !cfConnections.IsPendingReconnect || showRecentlyDisconnected {
numConnsPerColo[cfConnections.ColoName]++
}
}
// Get sorted list of colos
// nolint: prealloc
sortedColos := []string{}
for coloName := range numConnsPerColo {
sortedColos = append(sortedColos, coloName)
@@ -488,11 +489,12 @@ func readyCommand(c *cli.Context) error {
if err != nil {
return err
}
// nolint: gosec // URL is constructed from the user-configured local metrics endpoint.
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
defer func() { _ = res.Body.Close() }()
if res.StatusCode != 200 {
body, err := io.ReadAll(res.Body)
if err != nil {
@@ -613,7 +615,7 @@ func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error)
func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected bool) {
writer := tabWriter()
defer writer.Flush()
defer func() { _ = writer.Flush() }()
// Print the general tunnel info table
_, _ = fmt.Fprintf(writer, "NAME: %s\nID: %s\nCREATED: %s\n\n", tunnelInfo.Name, tunnelInfo.ID, tunnelInfo.CreatedAt)
@@ -654,14 +656,14 @@ func formatAndPrintConnectionsList(tunnelInfo Info, showRecentlyDisconnected boo
func tabWriter() *tabwriter.Writer {
const (
minWidth = 0
tabWidth = 8
padding = 1
padChar = ' '
flags = 0
minWidth = 0
tabWidth = 8
padding = 1
padChar = ' '
formatFlags = 0
)
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, formatFlags)
return writer
}
@@ -712,7 +714,8 @@ func renderOutput(format string, v interface{}) error {
}
func buildRunCommand() *cli.Command {
flags := []cli.Flag{
//nolint: prealloc
cliFlags := []cli.Flag{
credentialsFileFlag,
credentialsContentsFlag,
postQuantumFlag,
@@ -725,7 +728,7 @@ func buildRunCommand() *cli.Command {
maxActiveFlowsFlag,
dnsResolverAddrsFlag,
}
flags = append(flags, configureProxyFlags(false)...)
cliFlags = append(cliFlags, configureProxyFlags(false)...)
return &cli.Command{
Name: "run",
Action: cliutil.ConfiguredAction(runCommand),
@@ -740,7 +743,7 @@ func buildRunCommand() *cli.Command {
If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing
any old connection records.
`,
Flags: flags,
Flags: cliFlags,
CustomHelpTemplate: commandHelpTemplate(),
}
}
@@ -765,6 +768,7 @@ func runCommand(c *cli.Context) error {
// Check if tokenStr is blank before checking for tokenFile
if tokenStr == "" {
if tokenFile := c.String(TunnelTokenFileFlag); tokenFile != "" {
// nolint: gosec
data, err := os.ReadFile(tokenFile)
if err != nil {
return cliutil.UsageError("Failed to read token file: %s", err.Error())
@@ -1105,6 +1109,7 @@ func diagCommand(ctx *cli.Context) error {
Address: sctx.c.String(flags.Metrics),
ContainerID: sctx.c.String(diagContainerIDFlagName),
PodID: sctx.c.String(diagPodFlagName),
Region: sctx.c.String(flags.Region),
Toggles: diagnostic.Toggles{
NoDiagLogs: sctx.c.Bool(noDiagLogsFlagName),
NoDiagMetrics: sctx.c.Bool(noDiagMetricsFlagName),
+27 -37
View File
@@ -1,10 +1,9 @@
import json
import subprocess
from time import sleep
from constants import MANAGEMENT_HOST_NAME
from setup import get_config_from_file
from util import get_tunnel_connector_id
from util import get_tunnel_connector_id, CloudflaredProcess
SINGLE_CASE_TIMEOUT = 600
@@ -30,7 +29,7 @@ class CloudflaredCli:
listed = self._run_command(cmd_args, "list")
return json.loads(listed.stdout)
def get_management_token(self, config, config_path):
def get_management_token(self, config, config_path, resource):
basecmd = [config.cloudflared_binary]
if config_path is not None:
basecmd += ["--config", str(config_path)]
@@ -38,18 +37,35 @@ class CloudflaredCli:
if origincert:
basecmd += ["--origincert", origincert]
cmd_args = ["tail", "token", config.get_tunnel_id()]
cmd_args = ["management", "token", "--resource", resource, config.get_tunnel_id()]
cmd = basecmd + cmd_args
result = run_subprocess(cmd, "token", self.logger, check=True, capture_output=True, timeout=15)
return json.loads(result.stdout.decode("utf-8").strip())["token"]
def get_management_url(self, path, config, config_path):
access_jwt = self.get_management_token(config, config_path)
def get_tail_token(self, config, config_path):
"""
Get management token using the 'tail token' command.
Returns a token scoped for 'logs' resource.
"""
basecmd = [config.cloudflared_binary]
if config_path is not None:
basecmd += ["--config", str(config_path)]
origincert = get_config_from_file()["origincert"]
if origincert:
basecmd += ["--origincert", origincert]
cmd_args = ["tail", "token", config.get_tunnel_id()]
cmd = basecmd + cmd_args
result = run_subprocess(cmd, "tail-token", self.logger, check=True, capture_output=True, timeout=15)
return json.loads(result.stdout.decode("utf-8").strip())["token"]
def get_management_url(self, path, config, config_path, resource):
access_jwt = self.get_management_token(config, config_path, resource)
connector_id = get_tunnel_connector_id()
return f"https://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
def get_management_wsurl(self, path, config, config_path):
access_jwt = self.get_management_token(config, config_path)
def get_management_wsurl(self, path, config, config_path, resource):
access_jwt = self.get_management_token(config, config_path, resource)
connector_id = get_tunnel_connector_id()
return f"wss://{MANAGEMENT_HOST_NAME}/{path}?connector_id={connector_id}&access_token={access_jwt}"
@@ -66,38 +82,12 @@ class CloudflaredCli:
def __enter__(self):
self.basecmd += ["run"]
self.process = subprocess.Popen(self.basecmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.logger.info(f"Run cmd {self.basecmd}")
return self.process
self.cfd = CloudflaredProcess(self.basecmd, allow_input=False, capture_output=True)
return self.cfd
def __exit__(self, exc_type, exc_value, exc_traceback):
terminate_gracefully(self.process, self.logger, self.basecmd)
self.logger.debug(f"{self.basecmd} logs: {self.process.stderr.read()}")
def terminate_gracefully(process, logger, cmd):
process.terminate()
process_terminated = wait_for_terminate(process)
if not process_terminated:
process.kill()
logger.warning(f"{cmd}: cloudflared did not terminate within wait period. Killing process. logs: \
stdout: {process.stdout.read()}, stderr: {process.stderr.read()}")
def wait_for_terminate(opened_subprocess, attempts=10, poll_interval=1):
"""
wait_for_terminate polls the opened_subprocess every x seconds for a given number of attempts.
It returns true if the subprocess was terminated and false if it didn't.
"""
for _ in range(attempts):
if _is_process_stopped(opened_subprocess):
return True
sleep(poll_interval)
return False
def _is_process_stopped(process):
return process.poll() is not None
self.cfd.cleanup()
def cert_path():
+1 -8
View File
@@ -5,7 +5,7 @@ import base64
from dataclasses import dataclass, InitVar
from constants import METRICS_PORT, PROXY_DNS_PORT
from constants import METRICS_PORT
# frozen=True raises exception when assigning to fields. This emulates immutability
@@ -99,10 +99,3 @@ class QuickTunnelConfig(BaseConfig):
object.__setattr__(self, 'full_config',
self.merge_config(additional_config))
@dataclass(frozen=True)
class ProxyDnsConfig(BaseConfig):
full_config = {
"port": PROXY_DNS_PORT,
"no-autoupdate": True,
}
+3 -15
View File
@@ -5,15 +5,14 @@ from time import sleep
import pytest
import yaml
from config import NamedTunnelConfig, ProxyDnsConfig, QuickTunnelConfig
from constants import BACKOFF_SECS, PROXY_DNS_PORT
from config import NamedTunnelConfig, QuickTunnelConfig
from constants import BACKOFF_SECS
from util import LOGGER
class CfdModes(Enum):
NAMED = auto()
QUICK = auto()
PROXY_DNS = auto()
@pytest.fixture(scope="session")
@@ -26,16 +25,7 @@ def component_tests_config():
config = yaml.safe_load(stream)
LOGGER.info(f"component tests base config {config}")
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True, provide_ingress=True):
if run_proxy_dns:
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
# So we run all tests with it.
additional_config["proxy-dns"] = True
additional_config["proxy-dns-port"] = PROXY_DNS_PORT
else:
additional_config.pop("proxy-dns", None)
additional_config.pop("proxy-dns-port", None)
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, provide_ingress=True):
# Allows the ingress rules to be omitted from the provided config
ingress = []
if provide_ingress:
@@ -51,8 +41,6 @@ def component_tests_config():
credentials_file=config['credentials_file'],
ingress=ingress,
hostname=hostname)
elif cfd_mode is CfdModes.PROXY_DNS:
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
elif cfd_mode is CfdModes.QUICK:
return QuickTunnelConfig(additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'])
else:
+11 -1
View File
@@ -3,9 +3,19 @@ MAX_RETRIES = 5
BACKOFF_SECS = 7
MAX_LOG_LINES = 50
PROXY_DNS_PORT = 9053
MANAGEMENT_HOST_NAME = "management.argotunnel.com"
# How long to wait for the cloudflared process to exit after SIGTERM before
# sending SIGKILL.
GRACEFUL_SHUTDOWN_TIMEOUT = 10
# How long to wait for each pipe reader thread to finish after the process
# exits.
READER_THREAD_JOIN_TIMEOUT = 5
# How long to wait for an expected log message to appear before giving up.
LOG_POLL_TIMEOUT = 30
# How often to re-check the accumulated log lines while polling.
LOG_POLL_INTERVAL = 0.5
def protocols():
return ["http2", "quic"]
-10
View File
@@ -17,16 +17,6 @@ class TestEdgeDiscovery:
config["edge-ip-version"] = edge_ip_version
return config
@pytest.mark.parametrize("protocol", protocols())
def test_default_only(self, tmp_path, component_tests_config, protocol):
"""
This test runs a tunnel to connect via IPv4-only edge addresses (default is unset "--edge-ip-version 4")
"""
if self.has_ipv6_only():
pytest.skip("Host has IPv6 only support and current default is IPv4 only")
self.expect_address_connections(
tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
@pytest.mark.parametrize("protocol", protocols())
def test_ipv4_only(self, tmp_path, component_tests_config, protocol):
"""
+10 -7
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python
import json
import os
import time
from constants import MAX_LOG_LINES
from constants import MAX_LOG_LINES, LOG_POLL_INTERVAL, LOG_POLL_TIMEOUT
from util import start_cloudflared, wait_tunnel_ready, send_requests
# Rolling logger rotate log files after 1 MB
@@ -12,12 +13,14 @@ expect_message = "Starting Hello"
def assert_log_to_terminal(cloudflared):
for _ in range(0, MAX_LOG_LINES):
line = cloudflared.stderr.readline()
if not line:
break
if expect_message.encode() in line:
return
# All logs are drained by a background thread into cloudflared.stdout_lines.
# Poll the accumulated lines until the expected message appears.
deadline = time.monotonic() + LOG_POLL_TIMEOUT
while time.monotonic() < deadline:
for line in list(cloudflared.stdout_lines):
if expect_message.encode() in line:
return
time.sleep(LOG_POLL_INTERVAL)
raise Exception(f"terminal log doesn't contain {expect_message}")
+43 -9
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python
import json
import requests
from conftest import CfdModes
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
from retrying import retry
from cli import CloudflaredCli
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests, decode_jwt_payload
import platform
"""
@@ -25,7 +26,7 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
headers = {}
headers["Content-Type"] = "application/json"
@@ -35,7 +36,7 @@ class TestManagement:
require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
connector_id = cfd_cli.get_connector_id(config)[0]
url = cfd_cli.get_management_url("host_details", config, config_path)
url = cfd_cli.get_management_url("host_details", config, config_path, resource="host_details")
resp = send_request(url, headers=headers)
# Assert response json.
@@ -52,13 +53,13 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("metrics", config, config_path)
url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin")
resp = send_request(url)
# Assert response.
@@ -73,13 +74,13 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path)
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path, resource="admin")
resp = send_request(url)
# Assert response.
@@ -94,18 +95,51 @@ class TestManagement:
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("metrics", config, config_path)
url = cfd_cli.get_management_url("metrics", config, config_path, resource="admin")
resp = send_request(url)
# Assert response.
assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics"
def test_tail_token_command(self, tmp_path, component_tests_config):
"""
Validates that 'cloudflared tail token' command returns a token
scoped for 'logs' and 'ping' resources.
"""
# TUN-7377: wait_tunnel_ready does not work properly in windows
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
token = cfd_cli.get_tail_token(config, config_path)
# Verify token was returned
assert token, "Expected non-empty token to be returned"
# Decode JWT payload to verify resource claims
claims = decode_jwt_payload(token)
resource_tag = 'res'
# Verify the token has 'logs' and 'ping' in resource array
assert resource_tag in claims, f"Expected {resource_tag} claim in token"
assert isinstance(claims['res'], list), f"Expected {resource_tag} to be an array"
assert 'logs' in claims[resource_tag], \
f"Expected 'logs' in resource array, got: {claims[resource_tag]}"
assert 'ping' in claims[resource_tag], \
f"Expected 'ping' in resource array, got: {claims[resource_tag]}"
LOGGER.info(f"Tail token successfully verified with resources: {claims[resource_tag]}")
+541
View File
@@ -0,0 +1,541 @@
#!/usr/bin/env python
"""
Integration tests for cloudflared connectivity pre-checks (TUN-10391).
Scope
-----
These tests verify the end-to-end behavior of cloudflared pre-checks:
- that the human-readable table written to the log output has the correct
structure and content,
- that structured JSON log lines are emitted with the expected fields, and
- that running the `diag` subcommand against a live tunnel instance produces a
zip archive that contains prechecks.json.
They do NOT cover every failure mode of the precheck logic — those are owned
by the unit tests in prechecks/checker_test.go which use mock dialers.
At the integration level the only reliable way to induce specific failure modes
without real firewall intervention is:
- --edge <unreachable>: StaticEdgeDNSResolver resolves the literal IP
directly (DNS row = PASS), then both QUIC and HTTP/2 probes time out
-> hard fail (both transports blocked).
This does NOT exercise the DNS-failure -> transport-skip path.
DNS failure and Management API failure cannot be triggered via CLI flags alone;
they require network-level intervention outside the component-test harness.
stdout/stderr design
--------------------
The pre-checks table is emitted via cliutil.LogTable, which wraps the content
in an ASCII box and logs each line at Info level through zerolog. zerolog
writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
Popen). We poll a --logfile for the "precheck complete" sentinel before
leaving the `with` block, ensuring the goroutine has finished. We then call
cfd.terminate(). After the `with` block exits, the process is dead and all
output has been captured by CloudflaredProcess's background reader thread. We
read the accumulated lines from cfd.stdout_lines.
Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
+----...----+
| CONNECTIVITY PRE-CHECKS | (centered title)
+----...----+
| COMPONENT TARGET ... | (content rows)
...
+----...----+
"""
import json
import os
import re
import subprocess
import time
import zipfile as zipfilemod
from constants import METRICS_PORT
from util import LOGGER, start_cloudflared, wait_tunnel_ready
# ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
COL_HEADER = "COMPONENT" # first word of the column-header row
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
# Component names (probes.go: componentXxx)
COMP_DNS = "DNS Resolution"
COMP_QUIC = "UDP Connectivity"
COMP_H2 = "TCP Connectivity"
COMP_API = "Cloudflare API"
# Target labels used in the rendered table.
#
# probeRegion() (checker.go:216) always overwrites the Target field of
# whatever CheckResult the inner probe function returns with the regionTarget
# hostname, so QUIC and HTTP/2 rows carry the same region hostname as the
# corresponding DNS row — not the "Port 7844 (QUIC/HTTP2)" strings that
# targetPortQUIC/targetPortHTTP2 define. Those port-label constants are only
# used in the empty-addrs SKIP branch and inside action message strings.
TARGET_API = "api.cloudflare.com:443"
TARGET_REGION1 = "region1.v2.argotunnel.com"
TARGET_REGION2 = "region2.v2.argotunnel.com"
# Details strings (probes.go: detailsXxx)
DETAILS_DNS_RESOLVED = "DNS Resolved successfully"
DETAILS_QUIC_OK = "QUIC connection successful"
DETAILS_HTTP2_OK = "HTTP/2 connection successful"
DETAILS_API_OK = "API is reachable"
DETAILS_QUIC_FAIL = "QUIC connection failed"
DETAILS_HTTP2_FAIL = "HTTP/2 connection is blocked or unreachable"
# Status labels (result.go: xyzStatus)
PASS = "PASS"
FAIL = "FAIL"
SKIP = "SKIP"
# Action prefixes (result.go: renderActions)
PREFIX_ERROR = "ERROR: "
PREFIX_WARNING = "WARNING: "
# Action messages (probes.go: actionXxx)
ACTION_QUIC_BLOCKED = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
ACTION_HTTP2_BLOCKED = "Allow outbound TCP on port 7844."
# Exact summary lines (result.go: summaryLine)
SUMMARY_HEALTHY = "SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol."
SUMMARY_CRITICAL = "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
# structured log constants (result.go)
LOG_MSG_PRECHECK = "precheck"
LOG_MSG_PRECHECK_COMPLETE = "precheck complete"
STATUS_PASS_LOG = "pass"
UNREACHABLE_EDGE = "192.0.2.1:7844"
# cloudflared dial timeout per probe: 5 s, up to 2 retries -> ~15 s total.
PRECHECK_POLL_TIMEOUT_SECS = 15
PRECHECK_POLL_INTERVAL_SECS = 1
# ---------- helpers ----------
def _poll_log_file_for_precheck_complete(log_file: str, timeout: float) -> list[dict]:
"""
Poll a JSON log file until a 'precheck complete' line appears or timeout
expires. Returns all precheck-related log lines found.
cloudflared's --logfile writes one JSON object per line. Polling keeps
the test fast on healthy networks and still tolerates slow CI hosts.
We re-read from the beginning of the file on every poll because the file
is append-only, small, and tracking a byte offset would add complexity with
no meaningful performance benefit for a ~15 s total window.
"""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
lines = _read_precheck_log_lines_from_file(log_file)
if any(l.get("message") == LOG_MSG_PRECHECK_COMPLETE for l in lines):
return lines
time.sleep(PRECHECK_POLL_INTERVAL_SECS)
return _read_precheck_log_lines_from_file(log_file)
def _read_precheck_log_lines_from_file(log_file: str) -> list[dict]:
"""Parse all precheck-related JSON log lines from a --logfile path."""
result = []
try:
with open(log_file, "r") as f:
for raw_line in f:
raw_line = raw_line.strip()
if not raw_line:
continue
try:
obj = json.loads(raw_line)
except json.JSONDecodeError:
continue
msg = obj.get("message") or obj.get("msg", "")
if msg in (LOG_MSG_PRECHECK, LOG_MSG_PRECHECK_COMPLETE):
result.append(obj)
except FileNotFoundError:
pass
return result
# stdout table parse
class TableRow:
"""One data row parsed from the rendered precheck table."""
def __init__(self, component: str, target: str, status: str, details: str):
self.component = component
self.target = target
self.status = status
self.details = details
def __repr__(self):
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
def _strip_log_prefix(line: str) -> str:
"""Remove the zerolog console prefix ('2006-01-02T15:04:05Z LVL ') if present."""
return _LOG_PREFIX_RE.sub("", line, count=1)
def _unbox_line(line: str) -> str:
"""Strip the box border padding from a content line: '| text |' -> 'text'.
Accepts lines that may still carry a zerolog console prefix; the prefix is
removed before the box delimiters are stripped.
"""
msg = _strip_log_prefix(line)
if msg.startswith("|") and msg.endswith("|"):
return msg[1:-1].strip()
return msg.strip()
def _parse_table(stdout: str) -> list[TableRow]:
"""
Parse the data rows from a precheck table in stdout.
The table is now wrapped in an ASCII box by cliutil.LogTable. Each
content line has the form '| <content> |', optionally preceded by a
zerolog console prefix. We strip both the prefix and the box borders
before splitting on two-or-more spaces (text/tabwriter padding=2).
We skip the column-header row and stop at blank lines, SUMMARY, box
border lines, ERROR, or WARNING lines.
"""
rows = []
in_data = False
for raw_line in stdout.splitlines():
msg = _strip_log_prefix(raw_line)
line = _unbox_line(raw_line)
if line.startswith("COMPONENT"):
in_data = True
continue
if not in_data:
continue
if (line == "" or line.startswith("SUMMARY") or BOX_BORDER_RE.match(msg)
or line.startswith("ERROR") or line.startswith("WARNING")):
in_data = False
continue
parts = re.split(r" +", line.rstrip())
if len(parts) >= 3:
rows.append(TableRow(
component=parts[0],
target=parts[1],
status=parts[2],
details=parts[3] if len(parts) >= 4 else "",
))
return rows
def _rows_for(rows: list[TableRow], component: str) -> list[TableRow]:
return [r for r in rows if r.component == component]
# log assertions
def _assert_precheck_summary_log(
log_lines: list[dict],
*,
hard_fail: bool,
suggested_protocol: str | None = None,
):
"""Assert the 'precheck complete' summary log line has the expected fields."""
summary_lines = [l for l in log_lines if l.get("message") == LOG_MSG_PRECHECK_COMPLETE]
assert len(summary_lines) == 1, \
f"Expected exactly one '{LOG_MSG_PRECHECK_COMPLETE}' log line; got {summary_lines}"
summary = summary_lines[0]
assert summary.get("hard_fail") is hard_fail, \
f"Expected hard_fail={hard_fail} in summary log: {summary}"
if suggested_protocol is not None:
assert summary.get("suggested_protocol") == suggested_protocol, \
(f"Expected suggested_protocol={suggested_protocol!r}; "
f"got {summary.get('suggested_protocol')!r}")
# ---------- Tests ----------
class TestPrechecksHappyPath:
"""
On a healthy connection all probes pass. We assert:
- the full table structure (header, column header, separator)
- every row's component, target, status, and details
- no ERROR/WARNING action lines
- the exact summary line
- the structured log summary (hard_fail=false, suggested_protocol=quic)
"""
def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_config):
log_file = str(tmp_path / "cloudflared.log")
config = component_tests_config({"logfile": log_file})
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=["tunnel", "--ha-connections", "1"],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
# Poll the log file for the sentinel before signalling the process.
log_lines = _poll_log_file_for_precheck_complete(
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
)
# Signal shutdown.
cfd.terminate()
# The process is now dead. All output was captured by the background
# reader thread into cfd.stdout_lines (stderr is merged into stdout).
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
# Strip zerolog console prefixes so pattern matching works on raw messages.
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
# ── table structure ──────────────────────────────────────────────────
# zerolog writes to stderr which is merged into stdout by the harness.
# The table is wrapped in an ASCII box by cliutil.LogTable.
assert BOX_TITLE in messages, \
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
assert COL_HEADER in messages, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert BOX_BORDER_RE.search(messages), \
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
assert len(rows) == 7, \
f"Expected 7 rows (2 DNS + 2 QUIC + 2 HTTP/2 + 1 API); got {len(rows)}: {rows}"
dns_rows = _rows_for(rows, COMP_DNS)
assert len(dns_rows) == 2, f"Expected 2 DNS rows; got {dns_rows}"
assert dns_rows[0].target == TARGET_REGION1
assert dns_rows[1].target == TARGET_REGION2
for r in dns_rows:
assert r.status == PASS, f"DNS row not PASS: {r}"
assert r.details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {r}"
quic_rows = _rows_for(rows, COMP_QUIC)
assert len(quic_rows) == 2, f"Expected 2 QUIC rows; got {quic_rows}"
assert quic_rows[0].target == TARGET_REGION1, f"QUIC row[0] target wrong: {quic_rows[0]}"
assert quic_rows[1].target == TARGET_REGION2, f"QUIC row[1] target wrong: {quic_rows[1]}"
for r in quic_rows:
assert r.status == PASS, f"QUIC row not PASS: {r}"
assert r.details == DETAILS_QUIC_OK, f"QUIC row details wrong: {r}"
h2_rows = _rows_for(rows, COMP_H2)
assert len(h2_rows) == 2, f"Expected 2 HTTP/2 rows; got {h2_rows}"
assert h2_rows[0].target == TARGET_REGION1, f"HTTP/2 row[0] target wrong: {h2_rows[0]}"
assert h2_rows[1].target == TARGET_REGION2, f"HTTP/2 row[1] target wrong: {h2_rows[1]}"
for r in h2_rows:
assert r.status == PASS, f"HTTP/2 row not PASS: {r}"
assert r.details == DETAILS_HTTP2_OK, f"HTTP/2 row details wrong: {r}"
api_rows = _rows_for(rows, COMP_API)
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
# ── no action lines ──────────────────────────────────────────────────
assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
# ── summary line ─────────────────────────────────────────────────────
assert SUMMARY_HEALTHY in messages, \
f"Expected healthy summary;\ngot:\n{stdout}"
# ── structured log ───────────────────────────────────────────────────
assert len(log_lines) > 0, \
"Expected at least one structured precheck log line in log file"
for line in log_lines:
if line.get("message") == LOG_MSG_PRECHECK:
assert line.get("status") == STATUS_PASS_LOG, \
f"Expected status=pass in precheck log line: {line}"
_assert_precheck_summary_log(log_lines, hard_fail=False, suggested_protocol="quic")
class TestPrechecksHardFail:
"""
When --edge points at an unreachable IP, StaticEdgeDNSResolver resolves
the literal address directly (DNS row = PASS), but both transport probes
time out -> hard fail. We assert:
- the full table structure
- DNS row: PASS (the literal IP was resolved)
- QUIC row: FAIL with correct details + ERROR action
- HTTP/2 row: FAIL with correct details + ERROR action
- API row: PASS (api.cloudflare.com:443 is independently reachable)
- the exact critical summary line
- the structured log summary (hard_fail=true)
This test does NOT call wait_tunnel_ready because the tunnel will not
connect to the unreachable address.
"""
def test_prechecks_hard_fail_when_edge_unreachable(self, tmp_path, component_tests_config):
log_file = str(tmp_path / "cloudflared.log")
config = component_tests_config({"logfile": log_file})
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=[
"tunnel",
"--ha-connections", "1",
"--edge", UNREACHABLE_EDGE,
],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
log_lines = _poll_log_file_for_precheck_complete(
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
)
cfd.terminate()
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
# Strip zerolog console prefixes so pattern matching works on raw messages.
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
# ── table structure ──────────────────────────────────────────────────
# zerolog writes to stderr which is merged into stdout by the harness.
# The table is wrapped in an ASCII box by cliutil.LogTable.
assert BOX_TITLE in messages, \
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
assert COL_HEADER in messages, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert BOX_BORDER_RE.search(messages), \
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
assert len(rows) == 4, \
f"Expected 4 rows (1 DNS + 1 QUIC + 1 HTTP/2 + 1 API); got {len(rows)}: {rows}"
dns_rows = _rows_for(rows, COMP_DNS)
assert len(dns_rows) == 1, f"Expected 1 DNS row; got {dns_rows}"
assert dns_rows[0].target == UNREACHABLE_EDGE
assert dns_rows[0].status == PASS, f"DNS row not PASS: {dns_rows[0]}"
assert dns_rows[0].details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {dns_rows[0]}"
quic_rows = _rows_for(rows, COMP_QUIC)
assert len(quic_rows) == 1, f"Expected 1 QUIC row; got {quic_rows}"
assert quic_rows[0].target == UNREACHABLE_EDGE, f"QUIC row target wrong: {quic_rows[0]}"
assert quic_rows[0].status == FAIL, f"QUIC row not FAIL: {quic_rows[0]}"
assert quic_rows[0].details == DETAILS_QUIC_FAIL, f"QUIC row details wrong: {quic_rows[0]}"
h2_rows = _rows_for(rows, COMP_H2)
assert len(h2_rows) == 1, f"Expected 1 HTTP/2 row; got {h2_rows}"
assert h2_rows[0].target == UNREACHABLE_EDGE, f"HTTP/2 row target wrong: {h2_rows[0]}"
assert h2_rows[0].status == FAIL, f"HTTP/2 row not FAIL: {h2_rows[0]}"
assert h2_rows[0].details == DETAILS_HTTP2_FAIL, f"HTTP/2 row details wrong: {h2_rows[0]}"
api_rows = _rows_for(rows, COMP_API)
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in messages, \
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in messages, \
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
assert SUMMARY_CRITICAL in messages, \
f"Expected critical summary;\ngot:\n{stdout}"
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
class TestPreChecksDiag:
"""
Verify that `cloudflared tunnel diag` includes prechecks.json in the
diagnostic zip archive produced against a live tunnel instance.
The precheck job in diagnostic.go is gated on noDiagNetwork; we do NOT
pass --no-diag-network so prechecks.json must be present. We skip the
heavier collectors (logs, metrics, system, runtime) to keep the test fast.
The diag subcommand writes the zip to its current working directory. We
run it with cwd=tmp_path so the archive lands there and is cleaned up
automatically by pytest. We resolve config.cloudflared_binary to an
absolute path before changing cwd, because the binary path may be relative
to the original working directory.
"""
def test_diag_contains_prechecks_json(self, tmp_path, component_tests_config):
config = component_tests_config()
binary = os.path.abspath(config.cloudflared_binary)
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=["tunnel", "--ha-connections", "1"],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
# Run the diag subcommand as a one-shot process against the
# already-running instance. We skip log/metrics/system/runtime
# collectors; the network collector (which runs prechecks) is left
# enabled.
diag_result = subprocess.run(
[
binary,
"tunnel",
"diag",
"--metrics", f"localhost:{METRICS_PORT}",
"--no-diag-logs",
"--no-diag-metrics",
"--no-diag-system",
"--no-diag-runtime",
],
cwd=str(tmp_path),
capture_output=True,
timeout=60,
)
cfd.terminate()
diag_stdout = diag_result.stdout.decode(errors="replace")
diag_stderr = diag_result.stderr.decode(errors="replace")
LOGGER.debug(f"[diag] stdout:\n{diag_stdout}")
LOGGER.debug(f"[diag] stderr:\n{diag_stderr}")
assert diag_result.returncode == 0, (
f"cloudflared tunnel diag exited with code {diag_result.returncode}\n"
f"stdout:\n{diag_stdout}\nstderr:\n{diag_stderr}"
)
# Locate the zip file written to tmp_path by the diag command.
zip_files = list(tmp_path.glob("cloudflared-diag-*.zip"))
assert len(zip_files) == 1, \
f"Expected exactly one cloudflared-diag-*.zip in {tmp_path}; found {zip_files}"
zip_path = zip_files[0]
with zipfilemod.ZipFile(zip_path) as zf:
names = zf.namelist()
LOGGER.debug(f"[diag] zip contents: {names}")
assert "prechecks.json" in names, \
f"Expected prechecks.json in diag zip; got: {names}"
# Must be valid JSON containing at least the RunID field that
# prechecks.Run() always sets.
with zf.open("prechecks.json") as fh:
data = json.load(fh)
assert "RunID" in data, \
f"Expected RunID key in prechecks.json; got keys: {list(data.keys())}"
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env python
import socket
from time import sleep
import constants
from conftest import CfdModes
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
# Sanity checks that test that we only run Proxy DNS and Tunnel when we really expect them to be there.
class TestProxyDns:
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
expect_proxy_dns = run_proxy_dns
expect_tunnel = False
if cfd_mode == CfdModes.NAMED:
expect_tunnel = True
pre_args = ["tunnel", "--ha-connections", "1"]
args = ["run"]
elif cfd_mode == CfdModes.PROXY_DNS:
expect_proxy_dns = True
pre_args = []
args = ["proxy-dns", "--port", str(constants.PROXY_DNS_PORT)]
else:
assert False, f"Unknown cfd_mode {cfd_mode}"
config = component_tests_config(cfd_mode=cfd_mode, run_proxy_dns=run_proxy_dns)
with start_cloudflared(tmp_path, config, cfd_pre_args=pre_args, cfd_args=args, new_process=True, capture_output=False):
if expect_tunnel:
wait_tunnel_ready()
else:
check_tunnel_not_connected()
verify_proxy_dns(expect_proxy_dns)
def verify_proxy_dns(should_be_running):
# Wait for the Proxy DNS listener to come up.
sleep(constants.BACKOFF_SECS)
had_failure = False
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(('localhost', constants.PROXY_DNS_PORT))
sock.send(b"anything")
except:
if should_be_running:
assert False, "Expected Proxy DNS to be running, but it was not."
had_failure = True
finally:
sock.close()
if not should_be_running and not had_failure:
assert False, "Proxy DNS should not have been running, but it was."
+2 -14
View File
@@ -6,7 +6,7 @@ from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_u
class TestQuickTunnels:
def test_quick_tunnel(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
config = component_tests_config(cfd_mode=CfdModes.QUICK)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
@@ -15,22 +15,10 @@ class TestQuickTunnels:
send_requests(url, 3, True)
def test_quick_tunnel_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
config = component_tests_config(cfd_mode=CfdModes.QUICK)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
time.sleep(10)
url = get_quicktunnel_url()
send_requests(url+"/ready", 3, True)
def test_quick_tunnel_proxy_dns_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
LOGGER.debug(config)
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], expect_success=False)
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `hello-world`"
def test_quick_tunnel_proxy_dns_hello_world(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=True)
LOGGER.debug(config)
failed_start = start_cloudflared(tmp_path, config, cfd_args=["--hello-world"], expect_success=False)
assert failed_start.returncode == 1, "Expected cloudflared to fail to run with `proxy-dns` and `url`"
+10 -10
View File
@@ -19,13 +19,13 @@ class TestTail:
with the access token and start and stop streaming on-demand.
"""
print("test_start_stop_streaming")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=3) as websocket:
await websocket.send('{"type": "start_streaming"}')
await websocket.send('{"type": "stop_streaming"}')
@@ -38,13 +38,13 @@ class TestTail:
Validates that a streaming logs connection will stream logs
"""
print("test_streaming_logs")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# send start_streaming
await websocket.send(json.dumps({
@@ -65,13 +65,13 @@ class TestTail:
but not http when filters applied.
"""
print("test_streaming_logs_filters")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# send start_streaming with tcp logs only
await websocket.send(json.dumps({
@@ -92,13 +92,13 @@ class TestTail:
Validates that a streaming logs connection will stream logs with sampling.
"""
print("test_streaming_logs_sampling")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# send start_streaming with info logs only
await websocket.send(json.dumps({
@@ -120,13 +120,13 @@ class TestTail:
Validates that a streaming logs session can be overriden by the same actor
"""
print("test_streaming_logs_actor_override")
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_wsurl("logs", config, config_path)
url = cfd_cli.get_management_wsurl("logs", config, config_path, resource="logs")
task = asyncio.ensure_future(start_streaming_to_be_remotely_closed(url))
override_task = asyncio.ensure_future(start_streaming_override(url))
await asyncio.wait([task, override_task])
+3 -3
View File
@@ -11,14 +11,14 @@ class TestTunnel:
'''Test tunnels with no ingress rules from config.yaml but ingress rules from CLI only'''
def test_tunnel_hello_world(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=1)
def test_tunnel_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
@@ -29,7 +29,7 @@ class TestTunnel:
Running a tunnel with no ingress rules provided from either config.yaml or CLI will still work but return 503
for all incoming requests.
'''
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
config = component_tests_config(cfd_mode=CfdModes.NAMED, provide_ingress=False)
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
+108 -8
View File
@@ -2,6 +2,7 @@ import logging
import os
import platform
import subprocess
import threading
from contextlib import contextmanager
from time import sleep
import sys
@@ -12,7 +13,65 @@ import requests
import yaml
from retrying import retry
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS, GRACEFUL_SHUTDOWN_TIMEOUT, READER_THREAD_JOIN_TIMEOUT
class CloudflaredProcess:
"""
Wrapper around a Popen process that continuously drains stdout and stderr
in background threads to prevent OS pipe buffers from filling up and
blocking the child process. Captured output is logged when the process
is cleaned up.
"""
def __init__(self, cmd, allow_input, capture_output):
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
stdin = subprocess.PIPE if allow_input else None
self.process = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=subprocess.STDOUT)
self._capture_output = capture_output
self._stdout_lines = []
self._threads = []
if capture_output:
self._threads.append(self._start_reader(self.process.stdout, self._stdout_lines))
@staticmethod
def _start_reader(pipe, sink):
def _drain():
for line in pipe:
sink.append(line)
pipe.close()
t = threading.Thread(target=_drain, daemon=True)
t.start()
return t
def terminate(self):
"""Terminate the process if it is still running."""
if self.process.poll() is None:
self.process.terminate()
def cleanup(self):
"""Terminate, wait for exit, join reader threads, and log output."""
self.terminate()
try:
self.process.wait(timeout=GRACEFUL_SHUTDOWN_TIMEOUT)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
for t in self._threads:
t.join(timeout=READER_THREAD_JOIN_TIMEOUT)
if self._capture_output:
stdout = b"".join(self._stdout_lines).decode("utf-8", errors="replace")
if stdout:
LOGGER.info(f"cloudflared stdout:\n{stdout}")
@property
def stdout_lines(self):
return self._stdout_lines
# Proxy common Popen attributes so callers can still use the wrapper
# as if it were a Popen (e.g. send_signal, stdin, pid, returncode).
def __getattr__(self, name):
return getattr(self.process, name)
def configure_logger():
logger = logging.getLogger(__name__)
@@ -75,20 +134,15 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
LOGGER.info(f"Run cmd {cmd} with config {config}")
return cmd
@contextmanager
def run_cloudflared_background(cmd, allow_input, capture_output):
output = subprocess.PIPE if capture_output else subprocess.DEVNULL
stdin = subprocess.PIPE if allow_input else None
cfd = None
try:
cfd = subprocess.Popen(cmd, stdin=stdin, stdout=output, stderr=output)
cfd = CloudflaredProcess(cmd, allow_input, capture_output)
yield cfd
finally:
if cfd:
cfd.terminate()
if capture_output:
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
cfd.cleanup()
def get_quicktunnel_url():
@@ -185,3 +239,49 @@ def send_request(session, url, require_ok):
if require_ok:
assert resp.status_code == 200, f"{url} returned {resp}"
return resp if resp.status_code == 200 else None
def decode_jwt_payload(token):
"""
Decode the payload section of a JWT token without signature verification.
JWT Structure:
==============
A JWT consists of three Base64URL-encoded parts separated by dots:
HEADER.PAYLOAD.SIGNATURE
The payload contains the JWT claims (the actual data/permissions).
Args:
token (str): The complete JWT token string
Returns:
dict: The decoded payload as a dictionary containing JWT claims
Raises:
ValueError: If the token doesn't have exactly 3 parts
Note:
This function does NOT verify the signature - it only decodes the payload.
Use this only when you trust the token source (e.g., tokens you just generated).
"""
import base64
import json
# Split JWT into its three components
parts = token.split('.')
if len(parts) != 3:
raise ValueError(f"Invalid JWT format: expected 3 parts, got {len(parts)}")
# Extract and decode the payload (middle section)
# Base64 requires padding to be a multiple of 4 characters
payload_encoded = parts[1]
remainder = len(payload_encoded) % 4
if remainder != 0:
payload_padded = payload_encoded + '=' * (4 - remainder)
else:
payload_padded = payload_encoded
# Decode from Base64URL format and parse JSON
decoded_payload = base64.urlsafe_b64decode(payload_padded)
return json.loads(decoded_payload)
+2 -72
View File
@@ -4,9 +4,6 @@ import (
"crypto/sha256"
"fmt"
"io"
"strings"
"github.com/cloudflare/cloudflared/tunneldns"
)
// Forwarder represents a client side listener to forward traffic to the edge
@@ -26,23 +23,13 @@ type Tunnel struct {
ProtocolType string `json:"type"`
}
// DNSResolver represents a client side DNS resolver
type DNSResolver struct {
Enabled bool `json:"enabled"`
Address string `json:"address,omitempty"`
Port uint16 `json:"port,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"`
MaxUpstreamConnections int `json:"max_upstream_connections,omitempty"`
}
// Root is the base options to configure the service
// Root is the base options to configure the service.
type Root struct {
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
// `resolver` key is reserved for a removed feature (proxy-dns) and should not be used.
}
// Hash returns the computed values to see if the forwarder values change
@@ -55,60 +42,3 @@ func (f *Forwarder) Hash() string {
_, _ = io.WriteString(h, f.Destination)
return fmt.Sprintf("%x", h.Sum(nil))
}
// Hash returns the computed values to see if the forwarder values change
func (r *DNSResolver) Hash() string {
h := sha256.New()
_, _ = io.WriteString(h, r.Address)
_, _ = io.WriteString(h, strings.Join(r.Bootstraps, ","))
_, _ = io.WriteString(h, strings.Join(r.Upstreams, ","))
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.Port))
_, _ = io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections))
_, _ = io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
return fmt.Sprintf("%x", h.Sum(nil))
}
// EnabledOrDefault returns the enabled property
func (r *DNSResolver) EnabledOrDefault() bool {
return r.Enabled
}
// AddressOrDefault returns the address or returns the default if empty
func (r *DNSResolver) AddressOrDefault() string {
if r.Address != "" {
return r.Address
}
return "localhost"
}
// PortOrDefault return the port or returns the default if 0
func (r *DNSResolver) PortOrDefault() uint16 {
if r.Port > 0 {
return r.Port
}
return 53
}
// UpstreamsOrDefault returns the upstreams or returns the default if empty
func (r *DNSResolver) UpstreamsOrDefault() []string {
if len(r.Upstreams) > 0 {
return r.Upstreams
}
return []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"}
}
// BootstrapsOrDefault returns the bootstraps or returns the default if empty
func (r *DNSResolver) BootstrapsOrDefault() []string {
if len(r.Bootstraps) > 0 {
return r.Bootstraps
}
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
}
// MaxUpstreamConnectionsOrDefault return the max upstream connections or returns the default if negative
func (r *DNSResolver) MaxUpstreamConnectionsOrDefault() int {
if r.MaxUpstreamConnections >= 0 {
return r.MaxUpstreamConnections
}
return tunneldns.MaxUpstreamConnsDefault
}
+3 -1
View File
@@ -84,7 +84,7 @@ type TunnelToken struct {
}
func (t TunnelToken) Credentials() Credentials {
// nolint: gosimple
// nolint: staticcheck
return Credentials{
AccountTag: t.AccountTag,
TunnelSecret: t.TunnelSecret,
@@ -122,6 +122,7 @@ const (
// ShouldFlush returns whether this kind of connection should actively flush data
func (t Type) shouldFlush() bool {
// nolint: exhaustive
switch t {
case TypeWebsocket, TypeTCP, TypeControlStream:
return true
@@ -131,6 +132,7 @@ func (t Type) shouldFlush() bool {
}
func (t Type) String() string {
// nolint: exhaustive
switch t {
case TypeWebsocket:
return "websocket"
+2 -2
View File
@@ -146,8 +146,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
case <-wsCtx.Done():
case <-r.Context().Done():
}
readPipe.Close()
writePipe.Close()
_ = readPipe.Close()
_ = writePipe.Close()
}()
originConn := &echoPipe{reader: readPipe, writer: writePipe}
+9
View File
@@ -0,0 +1,9 @@
package dialopts
// DialOpts holds the configuration for dialing a QUIC connection.
type DialOpts struct {
// SkipPortReuse skips UDP port reuse. This is useful for probe connections
// that should use a random ephemeral port to avoid interfering with the
// main connection flow.
SkipPortReuse bool
}
+21 -9
View File
@@ -19,6 +19,9 @@ const (
edgeH2TLSServerName = "h2.cftunnel.com"
// edgeQUICServerName is the server name to establish quic connection with edge.
edgeQUICServerName = "quic.cftunnel.com"
// probeTLSServerName is the server name used for pre-flight connectivity checks.
probeTLSServerName = "probe.cftunnel.com"
quicProtos = "argotunnel"
AutoSelectFlag = "auto"
// SRV and TXT record resolution TTL
ResolveTTL = time.Hour
@@ -69,7 +72,24 @@ func (p Protocol) TLSSettings() *TLSSettings {
case QUIC:
return &TLSSettings{
ServerName: edgeQUICServerName,
NextProtos: []string{"argotunnel"},
NextProtos: []string{quicProtos},
}
default:
return nil
}
}
// ProbeTLSSettings returns TLS settings for pre-flight connectivity checks.
func (p Protocol) ProbeTLSSettings() *TLSSettings {
switch p {
case HTTP2:
return &TLSSettings{
ServerName: probeTLSServerName,
}
case QUIC:
return &TLSSettings{
ServerName: probeTLSServerName,
NextProtos: []string{quicProtos},
}
default:
return nil
@@ -204,18 +224,10 @@ func NewProtocolSelector(
protocolFlag string,
accountTag string,
tunnelTokenProvided bool,
needPQ bool,
protocolFetcher edgediscovery.PercentageFetcher,
resolveTTL time.Duration,
log *zerolog.Logger,
) (ProtocolSelector, error) {
// With --post-quantum, we force quic
if needPQ {
return &staticProtocolSelector{
current: QUIC,
}, nil
}
threshold := switchThreshold(accountTag)
fetchedProtocol, err := getProtocol(ProtocolList, protocolFetcher, threshold)
log.Debug().Msgf("Fetched protocol: %s", fetchedProtocol)
+54 -34
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/edgediscovery"
)
@@ -14,15 +15,6 @@ const (
testAccountTag = "testAccountTag"
)
func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) edgediscovery.PercentageFetcher {
return func() (edgediscovery.ProtocolPercents, error) {
if getError {
return nil, fmt.Errorf("failed to fetch percentage")
}
return protocolPercent, nil
}
}
type dynamicMockFetcher struct {
protocolPercents edgediscovery.ProtocolPercents
err error
@@ -39,7 +31,6 @@ func TestNewProtocolSelector(t *testing.T) {
name string
protocol string
tunnelTokenProvided bool
needPQ bool
expectedProtocol Protocol
hasFallback bool
expectedFallback Protocol
@@ -67,18 +58,6 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: true,
expectedFallback: HTTP2,
},
{
name: "named tunnel (post quantum)",
protocol: AutoSelectFlag,
needPQ: true,
expectedProtocol: QUIC,
},
{
name: "named tunnel (post quantum) w/http2",
protocol: "http2",
needPQ: true,
expectedProtocol: QUIC,
},
}
fetcher := dynamicMockFetcher{
@@ -87,16 +66,16 @@ func TestNewProtocolSelector(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
selector, err := NewProtocolSelector(test.protocol, testAccountTag, test.tunnelTokenProvided, test.needPQ, fetcher.fetch(), ResolveTTL, &log)
selector, err := NewProtocolSelector(test.protocol, testAccountTag, test.tunnelTokenProvided, fetcher.fetch(), ResolveTTL, &log)
if test.wantErr {
assert.Error(t, err, fmt.Sprintf("test %s failed", test.name))
assert.Error(t, err, "test %s failed", test.name)
} else {
assert.NoError(t, err, fmt.Sprintf("test %s failed", test.name))
assert.Equal(t, test.expectedProtocol, selector.Current(), fmt.Sprintf("test %s failed", test.name))
require.NoError(t, err, "test %s failed", test.name)
assert.Equalf(t, test.expectedProtocol, selector.Current(), "test %s failed", test.name)
fallback, ok := selector.Fallback()
assert.Equal(t, test.hasFallback, ok, fmt.Sprintf("test %s failed", test.name))
assert.Equalf(t, test.hasFallback, ok, "test %s failed", test.name)
if test.hasFallback {
assert.Equal(t, test.expectedFallback, fallback, fmt.Sprintf("test %s failed", test.name))
assert.Equalf(t, test.expectedFallback, fallback, "test %s failed", test.name)
}
}
})
@@ -105,8 +84,8 @@ func TestNewProtocolSelector(t *testing.T) {
func TestAutoProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, false, fetcher.fetch(), testNoTTL, &log)
require.NoError(t, err)
assert.Equal(t, QUIC, selector.Current())
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
@@ -135,8 +114,8 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{}
// Since the user chooses http2 on purpose, we always stick to it.
selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, false, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
selector, err := NewProtocolSelector(HTTP2.String(), testAccountTag, false, fetcher.fetch(), testNoTTL, &log)
require.NoError(t, err)
assert.Equal(t, HTTP2, selector.Current())
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
@@ -164,10 +143,51 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
func TestAutoProtocolSelectorNoRefreshWithToken(t *testing.T) {
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, false, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err)
selector, err := NewProtocolSelector(AutoSelectFlag, testAccountTag, true, fetcher.fetch(), testNoTTL, &log)
require.NoError(t, err)
assert.Equal(t, QUIC, selector.Current())
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
assert.Equal(t, QUIC, selector.Current())
}
func TestProbeTLSSettings(t *testing.T) {
tests := []struct {
name string
protocol Protocol
expectedServer string
expectedProtos []string
expectNil bool
}{
{
name: "HTTP2 returns probe SNI",
protocol: HTTP2,
expectedServer: probeTLSServerName,
expectedProtos: nil,
},
{
name: "QUIC returns probe SNI with alpn",
protocol: QUIC,
expectedServer: probeTLSServerName,
expectedProtos: []string{"argotunnel"},
},
{
name: "Unknown protocol returns nil",
protocol: Protocol(999),
expectNil: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
settings := test.protocol.ProbeTLSSettings()
if test.expectNil {
assert.Nil(t, settings)
} else {
assert.NotNil(t, settings)
assert.Equal(t, test.expectedServer, settings.ServerName)
assert.Equal(t, test.expectedProtos, settings.NextProtos)
}
})
}
}
+20 -26
View File
@@ -11,6 +11,9 @@ import (
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection/dialopts"
cfdquic "github.com/cloudflare/cloudflared/quic"
)
var (
@@ -26,8 +29,9 @@ func DialQuic(
localAddr net.IP,
connIndex uint8,
logger *zerolog.Logger,
) (quic.Connection, error) {
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, logger)
opts dialopts.DialOpts,
) (cfdquic.QUICConnection, error) {
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, opts, logger)
if err != nil {
return nil, err
}
@@ -35,22 +39,15 @@ func DialQuic(
conn, err := quic.Dial(ctx, udpConn, net.UDPAddrFromAddrPort(edgeAddr), tlsConfig, quicConfig)
if err != nil {
// close the udp server socket in case of error connecting to the edge
udpConn.Close()
_ = udpConn.Close()
return nil, &EdgeQuicDialError{Cause: err}
}
// 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, logger *zerolog.Logger) (*net.UDPConn, error) {
portMapMutex.Lock()
defer portMapMutex.Unlock()
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, opts dialopts.DialOpts, logger *zerolog.Logger) (*net.UDPConn, error) {
listenNetwork := "udp"
// https://github.com/quic-go/quic-go/issues/3793 DF bit cannot be set for dual stack listener ("udp") on macOS,
// to set the DF bit properly, the network string needs to be specific to the IP family.
@@ -62,15 +59,24 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.Add
}
}
// Probes skip port reuse entirely to avoid interfering with the main connection flow.
// They use a random ephemeral port for each dial.
if opts.SkipPortReuse {
return net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0})
}
portMapMutex.Lock()
defer portMapMutex.Unlock()
// if port was not set yet, it will be zero, so bind will randomly allocate one.
if port, ok := portForConnIndex[connIndex]; ok {
udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: port})
// if there wasn't an error, or if port was 0 (independently of error or not, just return)
if err == nil {
return udpConn, nil
} else {
logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex)
}
logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex)
}
// if we reached here, then there was an error or port as not been allocated it.
@@ -87,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
}
+6 -6
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,
@@ -143,7 +143,7 @@ func (q *quicConnection) Serve(ctx context.Context) error {
}
// serveControlStream will serve the RPC; blocking until the control plane is done.
func (q *quicConnection) serveControlStream(ctx context.Context, controlStream quic.Stream) error {
func (q *quicConnection) serveControlStream(ctx context.Context, controlStream *quic.Stream) error {
return q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions.ConnectionOptions(), q.orchestrator)
}
@@ -166,10 +166,10 @@ func (q *quicConnection) acceptStream(ctx context.Context) error {
}
}
func (q *quicConnection) runStream(quicStream quic.Stream) {
func (q *quicConnection) runStream(quicStream *quic.Stream) {
ctx := quicStream.Context()
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.
+105 -14
View File
@@ -29,6 +29,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/net/nettest"
"github.com/cloudflare/cloudflared/connection/dialopts"
"github.com/cloudflare/cloudflared/client"
"github.com/cloudflare/cloudflared/config"
cfdflow "github.com/cloudflare/cloudflared/flow"
@@ -149,7 +151,6 @@ func TestQUICServer(t *testing.T) {
}
for i, test := range tests {
test := test // capture range variable
t.Run(test.desc, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
// Start a UDP Listener for QUIC.
@@ -157,7 +158,7 @@ func TestQUICServer(t *testing.T) {
require.NoError(t, err)
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
require.NoError(t, err)
defer udpListener.Close()
defer func() { _ = udpListener.Close() }()
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
require.NoError(t, err)
@@ -499,7 +500,6 @@ func TestBuildHTTPRequest(t *testing.T) {
log := zerolog.Nop()
for _, test := range tests {
test := test // capture range variable
t.Run(test.name, func(t *testing.T) {
req, err := buildHTTPRequest(t.Context(), test.connectRequest, test.body, 0, &log)
require.NoError(t, err)
@@ -525,12 +525,12 @@ func TestServeUDPSession(t *testing.T) {
require.NoError(t, err)
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
require.NoError(t, err)
defer udpListener.Close()
defer func() { _ = udpListener.Close() }()
ctx, cancel := context.WithCancel(t.Context())
// Establish QUIC connection with edge
edgeQUICSessionChan := make(chan quic.Connection)
edgeQUICSessionChan := make(chan *quic.Conn)
go func() {
earlyListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig)
assert.NoError(t, err)
@@ -616,7 +616,7 @@ func TestTCPProxy_FlowRateLimited(t *testing.T) {
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
require.NoError(t, err)
defer udpListener.Close()
defer func() { _ = udpListener.Close() }()
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
@@ -660,7 +660,7 @@ func TestTCPProxy_FlowRateLimited(t *testing.T) {
func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPort) {
logger := zerolog.Nop()
conn, err := createUDPConnForConnIndex(0, nil, edgeIP, &logger)
conn, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
require.NoError(t, err)
getPortFunc := func(conn *net.UDPConn) int {
@@ -671,25 +671,115 @@ func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPo
initialPort := getPortFunc(conn)
// close conn
conn.Close()
_ = conn.Close()
// should get the same port as before.
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
require.NoError(t, err)
require.Equal(t, initialPort, getPortFunc(conn))
// new index, should get a different port
conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, &logger)
conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, dialopts.DialOpts{}, &logger)
require.NoError(t, err)
require.NotEqual(t, initialPort, getPortFunc(conn1))
// not closing the conn and trying to obtain a new conn for same index should give a different random port
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{}, &logger)
require.NoError(t, err)
require.NotEqual(t, initialPort, getPortFunc(conn))
}
func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) {
// TestSkipPortReuse tests that skipPortReuse uses a random ephemeral port for each dial.
func TestSkipPortReuse(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
edgeIP := netip.MustParseAddrPort("127.0.0.1:0")
// First dial with skipPortReuse should allocate a random port
conn1, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{SkipPortReuse: true}, &logger)
require.NoError(t, err)
port1 := conn1.LocalAddr().(*net.UDPAddr).Port
// Don't close conn1 yet - keep it open to prevent port reuse
// Second dial with skipPortReuse should allocate a different random port
conn2, err := createUDPConnForConnIndex(0, nil, edgeIP, dialopts.DialOpts{SkipPortReuse: true}, &logger)
require.NoError(t, err)
port2 := conn2.LocalAddr().(*net.UDPAddr).Port
// Now close both connections
_ = conn1.Close()
_ = conn2.Close()
// With skipPortReuse, ports should be different (random allocation)
require.NotEqual(t, port1, port2, "With skipPortReuse, each dial should use a different random port")
}
// TestDialQuicWithSkipPortReuse tests that DialQuic works correctly with the WithSkipPortReuse option.
func TestDialQuicWithSkipPortReuse(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
// Start a mock QUIC server (similar to TestQUICServer)
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
require.NoError(t, err)
defer func() { _ = udpListener.Close() }()
serverAddr := netip.MustParseAddrPort(udpListener.LocalAddr().String())
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
require.NoError(t, err)
serverDone := make(chan struct{})
go func() {
// Accept one connection
session, err := quicListener.Accept(ctx)
if err != nil {
close(serverDone)
return
}
// Keep session open until context is cancelled
<-ctx.Done()
_ = session.CloseWithError(0, "test done")
close(serverDone)
}()
// Test DialQuic with WithSkipPortReuse option
tlsClientConfig := &tls.Config{
// nolint: gosec
InsecureSkipVerify: true,
NextProtos: []string{"argotunnel"},
}
log := zerolog.New(io.Discard)
dialCtx, dialCancel := context.WithTimeout(t.Context(), 5*time.Second)
defer dialCancel()
// Dial with skipPortReuse option - should use a random ephemeral port
conn, err := DialQuic(
dialCtx,
testQUICConfig,
tlsClientConfig,
serverAddr,
nil, // connect on a random port
0,
&log,
dialopts.DialOpts{SkipPortReuse: true},
)
require.NoError(t, err)
require.NotNil(t, conn)
// Verify we can get connection state
_ = conn.ConnectionState()
// Clean up
_ = conn.CloseWithError(0, "test done")
cancel()
<-serverDone
}
func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQUICSession cfdquic.QUICConnection, closeType closeReason, expectedReason string, t *testing.T) {
payload := []byte(t.Name())
sessionID := uuid.New()
cfdConn, originConn := net.Pipe()
@@ -721,7 +811,7 @@ func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQ
// Close connection to terminate session
switch closeType {
case closedByOrigin:
originConn.Close()
_ = originConn.Close()
case closedByRemote:
err = datagramConn.UnregisterUdpSession(ctx, sessionID, expectedReason)
require.NoError(t, err)
@@ -753,7 +843,7 @@ const (
closedByTimeout
)
func runRPCServer(ctx context.Context, session quic.Connection, sessionRPCServer pogs.SessionManager, configRPCServer pogs.ConfigurationManager, t *testing.T) {
func runRPCServer(ctx context.Context, session cfdquic.QUICConnection, sessionRPCServer pogs.SessionManager, configRPCServer pogs.ConfigurationManager, t *testing.T) {
stream, err := session.AcceptStream(ctx)
require.NoError(t, err)
@@ -815,6 +905,7 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8)
nil, // connect on a random port
index,
&log,
dialopts.DialOpts{},
)
require.NoError(t, err)
+8 -13
View File
@@ -9,8 +9,6 @@ import (
"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"
@@ -25,7 +23,6 @@ import (
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,9 +31,7 @@ const (
demuxChanCapacity = 16
)
var (
errInvalidDestinationIP = errors.New("unable to parse destination IP")
)
var errInvalidDestinationIP = errors.New("unable to parse destination IP")
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
// connection streams.
@@ -47,7 +42,7 @@ type DatagramSessionHandler interface {
}
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,
@@ -116,7 +111,7 @@ func (d *datagramV2Connection) Serve(ctx context.Context) error {
}
// RegisterUdpSession is the RPC method invoked by edge to register and run a session
func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*tunnelpogs.RegisterUdpSessionResponse, error) {
func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) {
traceCtx := tracing.NewTracedContext(ctx, traceContext, q.logger)
ctx, registerSpan := traceCtx.Tracer().Start(traceCtx, "register-session", trace.WithAttributes(
attribute.String("session-id", sessionID.String()),
@@ -128,7 +123,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
if err := q.flowLimiter.Acquire(management.UDP.String()); err != nil {
log.Warn().Msgf("Too many concurrent sessions being handled, rejecting udp proxy to %s:%d", dstIP, dstPort)
err := pkgerrors.Wrap(err, "failed to start udp session due to rate limiting")
err := errors.Wrap(err, "failed to start udp session due to rate limiting")
tracing.EndWithErrorStatus(registerSpan, err)
return nil, err
}
@@ -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()
@@ -185,7 +180,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
Msgf("Registered session")
tracing.End(registerSpan)
resp := tunnelpogs.RegisterUdpSessionResponse{
resp := pogs.RegisterUdpSessionResponse{
Spans: traceCtx.GetProtoSpans(),
}
@@ -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
+2 -62
View File
@@ -1,13 +1,11 @@
package connection
import (
"context"
"net"
"testing"
"time"
"github.com/google/uuid"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
@@ -16,73 +14,15 @@ import (
"github.com/cloudflare/cloudflared/mocks"
)
type mockQuicConnection struct{}
func (m *mockQuicConnection) AcceptStream(_ context.Context) (quic.Stream, error) {
return nil, nil
}
func (m *mockQuicConnection) AcceptUniStream(_ context.Context) (quic.ReceiveStream, error) {
return nil, nil
}
func (m *mockQuicConnection) OpenStream() (quic.Stream, error) {
return nil, nil
}
func (m *mockQuicConnection) OpenStreamSync(_ context.Context) (quic.Stream, error) {
return nil, nil
}
func (m *mockQuicConnection) OpenUniStream() (quic.SendStream, error) {
return nil, nil
}
func (m *mockQuicConnection) OpenUniStreamSync(_ context.Context) (quic.SendStream, error) {
return nil, nil
}
func (m *mockQuicConnection) LocalAddr() net.Addr {
return nil
}
func (m *mockQuicConnection) RemoteAddr() net.Addr {
return nil
}
func (m *mockQuicConnection) CloseWithError(_ quic.ApplicationErrorCode, s string) error {
return nil
}
func (m *mockQuicConnection) Context() context.Context {
return nil
}
func (m *mockQuicConnection) ConnectionState() quic.ConnectionState {
panic("not meant to be called")
}
func (m *mockQuicConnection) SendDatagram(_ []byte) error {
return nil
}
func (m *mockQuicConnection) ReceiveDatagram(_ context.Context) ([]byte, error) {
return nil, nil
}
func (m *mockQuicConnection) AddPath(*quic.Transport) (*quic.Path, error) {
return nil, nil
}
func TestRateLimitOnNewDatagramV2UDPSession(t *testing.T) {
log := zerolog.Nop()
conn := &mockQuicConnection{}
ctrl := gomock.NewController(t)
flowLimiterMock := mocks.NewMockLimiter(ctrl)
connMock := mocks.NewMockQUICConnection(ctrl)
datagramConn := NewDatagramV2Connection(
t.Context(),
conn,
connMock,
nil,
nil,
0,
+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"
v3 "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 v3.DatagramConn
metrics v3.Metrics
logger *zerolog.Logger
}
func NewDatagramV3Connection(ctx context.Context,
conn quic.Connection,
sessionManager cfdquic.SessionManager,
conn cfdquic.QUICConnection,
sessionManager v3.SessionManager,
icmpRouter ingress.ICMPRouter,
index uint8,
metrics cfdquic.Metrics,
metrics v3.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 := v3.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
return &datagramV3Connection{
conn,
+79
View File
@@ -0,0 +1,79 @@
package crypto
import (
"crypto/tls"
"errors"
"fmt"
"slices"
"github.com/cloudflare/cloudflared/features"
)
// errUnknownPostQuantumMode is returned by GetCurvePreferences when the
// caller passes a features.PostQuantumMode value that is not one of the
// documented constants. It is intentionally unexported: callers should treat
// any non-nil error as a programming mistake rather than inspecting it.
var errUnknownPostQuantumMode = errors.New("the provided post quantum mode is unknown")
// P256Kyber768Draft00 is a post-quantum KEM based on Kyber768.
const P256Kyber768Draft00 = tls.CurveID(0xfe32) // ID 65074
// Canonical curve lists returned by GetCurvePreferences. They are kept
// package-private so that callers cannot accidentally mutate the shared
// slice; GetCurvePreferences always returns a clone.
var (
// postQuantumStrictCurves is used when the caller requires a
// post-quantum handshake. Only PQ curves (X25519MLKEM768 and the
// deprecated P256Kyber768Draft00 for backward compatibility) are
// advertised; no classical-only curve is included.
postQuantumStrictCurves = []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00}
// postQuantumPreferCurves is used for the default "prefer" mode: the PQ
// curve is advertised first and the classical CurveP256 is listed as a
// fallback so peers without PQ support can still negotiate.
postQuantumPreferCurves = []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00, tls.CurveP256}
)
// getCurvePreferences returns the TLS curve preferences that should be
// applied to edge-facing connections for the given post-quantum mode.
//
// The returned slice is the canonical, protocol-agnostic curve list and is
// suitable for direct assignment to tls.Config.CurvePreferences. A fresh
// slice is returned on every call, so callers may mutate it freely without
// affecting other callers.
//
// An error is returned only when profile is not a recognised
// features.PostQuantumMode value, which indicates a programming bug in the
// caller.
func getCurvePreferences(profile features.PostQuantumMode) ([]tls.CurveID, error) {
switch profile {
case features.PostQuantumPrefer:
return slices.Clone(postQuantumPreferCurves), nil
case features.PostQuantumStrict:
return slices.Clone(postQuantumStrictCurves), nil
}
return nil, errUnknownPostQuantumMode
}
// TLSConfigWithCurvePreferences clones the provided tls.Config and applies
// curve preferences based on the given post-quantum mode.
//
// The original tls.Config is never modified; a clone is returned so that
// callers can safely use the same base configuration across multiple
// goroutines without racing on CurvePreferences.
//
// Returns an error only when pqMode is not a recognised
// features.PostQuantumMode value.
func TLSConfigWithCurvePreferences(tlsConfig *tls.Config, pqMode features.PostQuantumMode) (*tls.Config, error) {
// Clone the TLS config before applying per-connection curve
// preferences. The TlsConfig may be shared across goroutines;
// mutating it directly would race with concurrent connection attempts.
config := tlsConfig.Clone()
curvePref, err := getCurvePreferences(pqMode)
if err != nil {
return nil, fmt.Errorf("get curve preferences: %w", err)
}
config.CurvePreferences = curvePref
return config, nil
}
+126
View File
@@ -0,0 +1,126 @@
package crypto
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"runtime"
"slices"
"testing"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/features"
)
// TestCurvePreferences verifies that GetCurvePreferences returns the
// documented curve list for each supported PostQuantumMode. The expected
// values correspond to the contract described in the package documentation
// and must be identical under FIPS and non-FIPS builds (see TUN-10413).
func TestCurvePreferences(t *testing.T) {
t.Parallel()
tests := []struct {
name string
expectedCurves []tls.CurveID
pqMode features.PostQuantumMode
}{
{
name: "Prefer PQ",
pqMode: features.PostQuantumPrefer,
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00, tls.CurveP256},
},
{
name: "Strict PQ",
pqMode: features.PostQuantumStrict,
expectedCurves: []tls.CurveID{tls.X25519MLKEM768, P256Kyber768Draft00},
},
}
for _, tcase := range tests {
t.Run(tcase.name, func(t *testing.T) {
t.Parallel()
curves, err := getCurvePreferences(tcase.pqMode)
require.NoError(t, err)
require.Equal(t, tcase.expectedCurves, curves)
})
}
}
// TestCurvePreferenceUnknownMode asserts that passing a PostQuantumMode
// value outside of the documented constants produces an error instead of
// silently returning a nil or default curve list. This protects callers
// from accidentally negotiating with an unintended curve set.
func TestCurvePreferenceUnknownMode(t *testing.T) {
t.Parallel()
_, err := getCurvePreferences(features.PostQuantumMode(255))
require.Error(t, err)
}
// TestReturnedSliceIsIndependent ensures GetCurvePreferences returns a
// fresh slice on every call, so that callers cannot corrupt the
// package-level defaults by mutating the result.
func TestReturnedSliceIsIndependent(t *testing.T) {
t.Parallel()
first, err := getCurvePreferences(features.PostQuantumPrefer)
require.NoError(t, err)
// Mutate the returned slice.
first[0] = tls.CurveP521
second, err := getCurvePreferences(features.PostQuantumPrefer)
require.NoError(t, err)
require.Equal(t, tls.X25519MLKEM768, second[0], "package defaults must not be affected by caller mutation")
}
// runClientServerHandshake drives a TLS 1.3 handshake with the given curve
// preferences set on the client and captures the SupportedCurves list
// advertised by the client in its ClientHello. The helper is used by
// TestSupportedCurvesNegotiation to exercise the curves end-to-end against
// the standard library's TLS stack.
func runClientServerHandshake(t *testing.T, curves []tls.CurveID) []tls.CurveID {
var advertisedCurves []tls.CurveID
ts := httptest.NewUnstartedServer(nil)
ts.TLS = &tls.Config{ // nolint: gosec
GetConfigForClient: func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
advertisedCurves = slices.Clone(chi.SupportedCurves)
return nil, nil
},
}
ts.StartTLS()
defer ts.Close()
clientTLSConfig := ts.Client().Transport.(*http.Transport).TLSClientConfig
clientTLSConfig.CurvePreferences = curves
resp, err := ts.Client().Head(ts.URL)
if err != nil {
t.Error(err)
return nil
}
defer func() { _ = resp.Body.Close() }()
return advertisedCurves
}
// TestSupportedCurvesNegotiation verifies that the curves returned by
// GetCurvePreferences survive a real TLS handshake unchanged, i.e. the
// standard library advertises exactly the curves we expect. Currently only
// PostQuantumPrefer is exercised because PostQuantumStrict would cause the
// handshake to fail against httptest servers that do not support
// X25519MLKEM768 server-side.
func TestSupportedCurvesNegotiation(t *testing.T) {
t.Parallel()
for _, tcase := range []features.PostQuantumMode{features.PostQuantumPrefer} {
curves, err := getCurvePreferences(tcase)
require.NoError(t, err)
advertisedCurves := runClientServerHandshake(t, curves)
require.True(t, slices.Contains(advertisedCurves, tls.CurveP256))
require.True(t, slices.Contains(advertisedCurves, tls.X25519MLKEM768))
expectedLength := 2
if runtime.GOOS == "linux" {
// P256Kyber768Draft00 only exists in linux
require.True(t, slices.Contains(advertisedCurves, P256Kyber768Draft00))
expectedLength = 3
}
require.Len(t, advertisedCurves, expectedLength)
}
}
+35
View File
@@ -0,0 +1,35 @@
// Package crypto centralizes the cryptographic primitives and TLS
// configuration used by cloudflared when establishing connections to the
// Cloudflare edge.
//
// The primary responsibility of the package is to expose a single, canonical
// source of TLS curve preferences so that every edge-facing transport (QUIC
// and HTTP/2) negotiates the same key-exchange algorithms regardless of the
// code path that sets up the connection.
//
// # Post-Quantum key exchange
//
// cloudflared supports the X25519MLKEM768 hybrid post-quantum key exchange.
// Two operating modes are exposed via the features.PostQuantumMode flag:
//
// - PostQuantumPrefer: advertise X25519MLKEM768 and the deprecated
// P256Kyber768Draft00 first, then fall back to the classical CurveP256
// if the peer does not support either PQ curve. This is the default
// used for every outbound edge connection.
// - PostQuantumStrict: advertise only the PQ curves (X25519MLKEM768 and
// P256Kyber768Draft00). Activated by the user via the --post-quantum
// CLI flag. No classical fallback is offered, so a peer that does not
// support any PQ curve will fail the handshake.
//
// The resulting curve lists are identical under FIPS and non-FIPS builds,
// which is why GetCurvePreferences does not take a FIPS toggle. If that
// property ever changes (for example, if a curve stops being FIPS-approved),
// the divergence should be expressed inside this package so callers remain
// unchanged.
//
// # Thread-safety
//
// GetCurvePreferences returns a fresh slice on every call. Callers are free
// to mutate the returned slice without affecting the package-level defaults
// or other callers.
package crypto
+1
View File
@@ -34,4 +34,5 @@ const (
cliConfigurationBaseName = "cli-configuration.json"
configurationBaseName = "configuration.json"
taskResultBaseName = "task-result.json"
prechecksBaseName = "prechecks.json"
)
+65 -9
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"path/filepath"
@@ -16,6 +17,8 @@ import (
"github.com/rs/zerolog"
network "github.com/cloudflare/cloudflared/diagnostic/network"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/prechecks"
)
const (
@@ -32,6 +35,7 @@ const (
networkInformationJobName = "network information"
cliConfigurationJobName = "cli configuration"
configurationJobName = "configuration"
prechecksJobName = "connectivity pre-checks"
)
// Struct used to hold the results of different routines executing the network collection.
@@ -92,6 +96,7 @@ type Options struct {
Address string
ContainerID string
PodID string
Region string
Toggles Toggles
}
@@ -126,13 +131,14 @@ func collectLogs(
if err != nil {
return "", fmt.Errorf("error opening log file while collecting logs: %w", err)
}
defer logHandle.Close()
defer func() { _ = logHandle.Close() }()
// nolint: gosec
outputLogHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
if err != nil {
return "", ErrCreatingTemporaryFile
}
defer outputLogHandle.Close()
defer func() { _ = outputLogHandle.Close() }()
_, err = io.Copy(outputLogHandle, logHandle)
if err != nil {
@@ -229,12 +235,13 @@ func networkInformationCollectors() (rawNetworkCollector, jsonNetworkCollector c
}
func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) {
// nolint: gosec // Intentionally creating a temporary diagnostic file in the OS temp directory.
networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), rawNetworkBaseName))
if err != nil {
return "", ErrCreatingTemporaryFile
}
defer networkDumpHandle.Close()
defer func() { _ = networkDumpHandle.Close() }()
var exitErr error
@@ -260,12 +267,13 @@ func rawNetworkInformationWriter(resultMap map[string]networkCollectionResult) (
}
func jsonNetworkInformationWriter(resultMap map[string]networkCollectionResult) (string, error) {
// nolint: gosec
networkDumpHandle, err := os.Create(filepath.Join(os.TempDir(), networkBaseName))
if err != nil {
return "", ErrCreatingTemporaryFile
}
defer networkDumpHandle.Close()
defer func() { _ = networkDumpHandle.Close() }()
encoder := newFormattedEncoder(networkDumpHandle)
@@ -290,11 +298,12 @@ func jsonNetworkInformationWriter(resultMap map[string]networkCollectionResult)
func collectFromEndpointAdapter(collect collectToWriterFunc, fileName string) collectFunc {
return func(ctx context.Context) (string, error) {
// nolint: gosec
dumpHandle, err := os.Create(filepath.Join(os.TempDir(), fileName))
if err != nil {
return "", ErrCreatingTemporaryFile
}
defer dumpHandle.Close()
defer func() { _ = dumpHandle.Close() }()
err = collect(ctx, dumpHandle)
if err != nil {
@@ -349,12 +358,12 @@ func resolveInstanceBaseURL(
if !strings.HasPrefix(metricsServerAddress, "http://") {
metricsServerAddress = "http://" + metricsServerAddress
}
url, err := url.Parse(metricsServerAddress)
baseUrl, err := url.Parse(metricsServerAddress)
if err != nil {
return nil, nil, nil, fmt.Errorf("provided address is not valid: %w", err)
}
return url, nil, nil, nil
return baseUrl, nil, nil, nil
}
tunnelState, foundTunnelStates, err := FindMetricsServer(log, client, addresses)
@@ -368,6 +377,7 @@ func resolveInstanceBaseURL(
func createJobs(
client *httpClient,
tunnel *TunnelState,
region string,
diagContainer string,
diagPod string,
noDiagSystem bool,
@@ -430,17 +440,62 @@ func createJobs(
fn: collectFromEndpointAdapter(client.GetTunnelConfiguration, configurationBaseName),
bypass: false,
},
{
jobName: prechecksJobName,
fn: collectPrechecks(region),
bypass: noDiagNetwork,
},
}
return jobs
}
// collectPrechecks runs connectivity pre-checks and writes the results to a JSON file.
func collectPrechecks(region string) collectFunc {
return func(ctx context.Context) (string, error) {
cfg := prechecks.Config{
Region: region,
IPVersion: allregions.Auto,
Timeout: defaultTimeout,
}
// Create a no-op logger since we don't want to spam logs during diagnostic collection
log := zerolog.New(io.Discard)
dialers := prechecks.RunDialers{
DNSResolver: &prechecks.EdgeDNSResolver{Log: &log},
TCPDialer: &prechecks.EdgeTCPDialer{},
QUICDialer: &prechecks.EdgeQUICDialer{},
ManagementDialer: &prechecks.NetManagementDialer{Dialer: net.Dialer{}},
}
emptyCert := ""
report := prechecks.Run(ctx, emptyCert, cfg, &log, dialers)
// Write the report to a JSON file
// nolint: gosec
dumpHandle, err := os.Create(filepath.Join(os.TempDir(), prechecksBaseName))
if err != nil {
return "", ErrCreatingTemporaryFile
}
defer func() { _ = dumpHandle.Close() }()
encoder := newFormattedEncoder(dumpHandle)
if err := encoder.Encode(report); err != nil {
return dumpHandle.Name(), fmt.Errorf("error encoding prechecks report: %w", err)
}
return dumpHandle.Name(), nil
}
}
func createTaskReport(taskReport map[string]taskResult) (string, error) {
// nolint: gosec
dumpHandle, err := os.Create(filepath.Join(os.TempDir(), taskResultBaseName))
if err != nil {
return "", ErrCreatingTemporaryFile
}
defer dumpHandle.Close()
defer func() { _ = dumpHandle.Close() }()
encoder := newFormattedEncoder(dumpHandle)
@@ -522,6 +577,7 @@ func RunDiagnostic(
jobs := createJobs(
client,
tunnel,
options.Region,
options.ContainerID,
options.PodID,
options.Toggles.NoDiagSystem,
@@ -545,7 +601,7 @@ func RunDiagnostic(
defer func() {
if !errors.Is(v.Err, ErrCreatingTemporaryFile) {
os.Remove(v.path)
_ = os.Remove(v.path)
}
}()
}
+4 -4
View File
@@ -20,18 +20,18 @@ func NewDockerLogCollector(containerID string) *DockerLogCollector {
}
func (collector *DockerLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
tmp := os.TempDir()
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
// nolint: gosec
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
if err != nil {
return nil, fmt.Errorf("error opening output file: %w", err)
}
defer outputHandle.Close()
defer func() { _ = outputHandle.Close() }()
// Calculate 2 weeks ago
since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339)
// nolint: gosec
command := exec.CommandContext(
ctx,
"docker",
+3 -5
View File
@@ -13,7 +13,6 @@ const (
linuxManagedLogsPath = "/var/log/cloudflared.err"
darwinManagedLogsPath = "/Library/Logs/com.cloudflare.cloudflared.err.log"
linuxServiceConfigurationPath = "/etc/systemd/system/cloudflared.service"
linuxSystemdPath = "/run/systemd/system"
)
type HostLogCollector struct {
@@ -27,14 +26,13 @@ func NewHostLogCollector(client HTTPClient) *HostLogCollector {
}
func extractLogsFromJournalCtl(ctx context.Context) (*LogInformation, error) {
tmp := os.TempDir()
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
// nolint: gosec
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
if err != nil {
return nil, fmt.Errorf("error opening output file: %w", err)
}
defer outputHandle.Close()
defer func() { _ = outputHandle.Close() }()
command := exec.CommandContext(
ctx,
+5 -3
View File
@@ -22,18 +22,19 @@ func NewKubernetesLogCollector(containerID, pod string) *KubernetesLogCollector
}
func (collector *KubernetesLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
tmp := os.TempDir()
outputHandle, err := os.Create(filepath.Join(tmp, logFilename))
// nolint: gosec
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
if err != nil {
return nil, fmt.Errorf("error opening output file: %w", err)
}
defer outputHandle.Close()
defer func() { _ = outputHandle.Close() }()
var command *exec.Cmd
// Calculate 2 weeks ago
since := time.Now().Add(twoWeeksOffset).Format(time.RFC3339)
if collector.containerID != "" {
// nolint: gosec
command = exec.CommandContext(
ctx,
"kubectl",
@@ -47,6 +48,7 @@ func (collector *KubernetesLogCollector) Collect(ctx context.Context) (*LogInfor
collector.containerID,
)
} else {
// nolint: gosec
command = exec.CommandContext(
ctx,
"kubectl",
+13 -9
View File
@@ -67,6 +67,8 @@ func PipeCommandOutputToFile(command *exec.Cmd, outputHandle *os.File) (*LogInfo
}
func CopyFilesFromDirectory(path string) (string, error) {
const defaultLogFilename = "cloudflared.log"
// rolling logs have as suffix the current date thus
// when iterating the path files they are already in
// chronological order
@@ -75,30 +77,32 @@ func CopyFilesFromDirectory(path string) (string, error) {
return "", fmt.Errorf("error reading directory %s: %w", path, err)
}
// nolint: gosec
outputHandle, err := os.Create(filepath.Join(os.TempDir(), logFilename))
if err != nil {
return "", fmt.Errorf("creating file %s: %w", outputHandle.Name(), err)
return "", fmt.Errorf("creating temporary log file %s: %w", logFilename, err)
}
defer outputHandle.Close()
defer func() { _ = outputHandle.Close() }()
for _, file := range files {
// nolint: gosec
logHandle, err := os.Open(filepath.Join(path, file.Name()))
if err != nil {
return "", fmt.Errorf("error opening file %s:%w", file.Name(), err)
return "", fmt.Errorf("error opening file %s: %w", file.Name(), err)
}
defer logHandle.Close()
_, err = io.Copy(outputHandle, logHandle)
_ = logHandle.Close()
if err != nil {
return "", fmt.Errorf("error copying file %s:%w", logHandle.Name(), err)
return "", fmt.Errorf("error copying file %s: %w", file.Name(), err)
}
}
logHandle, err := os.Open(filepath.Join(path, "cloudflared.log"))
// nolint: gosec
logHandle, err := os.Open(filepath.Join(path, defaultLogFilename))
if err != nil {
return "", fmt.Errorf("error opening file %s:%w", logHandle.Name(), err)
return "", fmt.Errorf("error opening file %s:%w", defaultLogFilename, err)
}
defer logHandle.Close()
defer func() { _ = logHandle.Close() }()
_, err = io.Copy(outputHandle, logHandle)
if err != nil {
+1 -1
View File
@@ -109,7 +109,7 @@ var friendlyDNSErrorLines = []string{
}
// EdgeDiscovery implements HA service discovery lookup.
func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
func EdgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
logger := log.With().Int(management.EventTypeKey, int(management.Cloudflared)).Logger()
logger.Debug().
Int(management.EventTypeKey, int(management.Cloudflared)).
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func (ea *EdgeAddr) String() string {
@@ -25,8 +26,8 @@ func TestEdgeDiscovery(t *testing.T) {
}
l := zerolog.Nop()
addrLists, err := edgeDiscovery(&l, "")
assert.NoError(t, err)
addrLists, err := EdgeDiscovery(&l, "")
require.NoError(t, err)
actualAddrSet := map[string]bool{}
for _, addrs := range addrLists {
for _, addr := range addrs {
+8 -5
View File
@@ -20,7 +20,7 @@ type Regions struct {
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
func ResolveEdge(log *zerolog.Logger, region string, overrideIPVersion ConfigIPVersion) (*Regions, error) {
edgeAddrs, err := edgeDiscovery(log, getRegionalServiceName(region))
edgeAddrs, err := EdgeDiscovery(log, RegionalServiceName(region))
if err != nil {
return nil, err
}
@@ -91,6 +91,7 @@ func (rs *Regions) GetUnusedAddr(excluding *EdgeAddr, connID int) *EdgeAddr {
// evenly across both regions.
if rs.region1.AvailableAddrs() == rs.region2.AvailableAddrs() {
regions := []Region{rs.region1, rs.region2}
//nolint:gosec
firstChoice := rand.Intn(2)
return getAddrs(excluding, connID, &regions[firstChoice], &regions[1-firstChoice])
}
@@ -131,11 +132,13 @@ func (rs *Regions) GiveBack(addr *EdgeAddr, hasConnectivityError bool) bool {
return rs.region2.GiveBack(addr, hasConnectivityError)
}
// Return regionalized service name if `region` isn't empty, otherwise return the global service name for origintunneld
func getRegionalServiceName(region string) string {
// RegionalServiceName returns the SRV service name for the given region.
// When region is empty it returns the global service name ("v2-origintunneld").
// Otherwise, it prepends the region, e.g. "us-v2-origintunneld".
func RegionalServiceName(region string) string {
if region != "" {
return region + "-" + srvService // Example: `us-v2-origintunneld`
return region + "-" + srvService
}
return srvService // Global service is just `v2-origintunneld`
return srvService
}
+4 -6
View File
@@ -237,21 +237,19 @@ func TestNewNoResolveBalancesRegions(t *testing.T) {
}
}
func TestGetRegionalServiceName(t *testing.T) {
func TestRegionalServiceName(t *testing.T) {
// Empty region should just go to origintunneld
globalServiceName := getRegionalServiceName("")
assert.Equal(t, srvService, globalServiceName)
assert.Equal(t, srvService, RegionalServiceName(""))
// Non-empty region should go to the regional origintunneld variant
for _, region := range []string{"us", "pt", "am"} {
regionalServiceName := getRegionalServiceName(region)
assert.Equal(t, region+"-"+srvService, regionalServiceName)
assert.Equal(t, region+"-"+srvService, RegionalServiceName(region))
}
}
func RegionsIsBalanced(t *testing.T, rs *Regions) {
delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs()
assert.True(t, abs(delta) <= 1)
assert.LessOrEqual(t, abs(delta), 1)
}
func abs(x int) int {
+4
View File
@@ -42,6 +42,10 @@ type FeatureSnapshot struct {
// We provide the list of features since we need it to send in the ConnectionOptions during connection
// registrations.
FeaturesList []string
// SkipPrechecks indicates when to skip connectivity pre-checks at startup.
// Controlled via DNS TXT record to allow remote kill-switch in case of issues.
SkipPrechecks bool
}
type PostQuantumMode uint8
+9 -1
View File
@@ -24,6 +24,7 @@ const (
type featuresRecord struct {
DatagramV3Percentage uint32 `json:"dv3_2"`
SkipPrechecks bool `json:"skip_prechecks"`
// DatagramV3Percentage int32 `json:"dv3"` // Removed in TUN-9291
// DatagramV3Percentage uint32 `json:"dv3_1"` // Removed in TUN-9883
@@ -89,6 +90,7 @@ func (fs *featureSelector) Snapshot() FeatureSnapshot {
PostQuantum: fs.postQuantumMode(),
DatagramVersion: fs.datagramVersion(),
FeaturesList: fs.clientFeatures(),
SkipPrechecks: fs.prechecksSkip(),
}
}
@@ -121,6 +123,12 @@ func (fs *featureSelector) datagramVersion() DatagramVersion {
return DatagramV2
}
// prechecksSkip returns whether prechecks are enabled via DNS flag.
// Defaults to false if not set in the DNS TXT record.
func (fs *featureSelector) prechecksSkip() bool {
return fs.remoteFeatures.SkipPrechecks
}
// clientFeatures will return the list of currently available features that cloudflared should provide to the edge.
func (fs *featureSelector) clientFeatures() []string {
// Evaluate any remote features along with static feature list to construct the list of features
@@ -186,7 +194,7 @@ func (dr *dnsResolver) lookupRecord(ctx context.Context) ([]byte, error) {
}
if len(records) == 0 {
return nil, fmt.Errorf("No TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
return nil, fmt.Errorf("no TXT record found for %s to determine which features to opt-in", featureSelectorHostname)
}
return []byte(records[0]), nil
+39 -43
View File
@@ -1,47 +1,47 @@
module github.com/cloudflare/cloudflared
go 1.24
go 1.26
require (
github.com/coredns/coredns v1.12.2
github.com/coreos/go-oidc/v3 v3.10.0
github.com/cloudflare/backoff v0.0.0-20240920015135-e46b80a3a7d0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/coreos/go-systemd/v22 v22.5.0
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
github.com/fortytw2/leaktest v1.3.0
github.com/fsnotify/fsnotify v1.4.9
github.com/getsentry/sentry-go v0.16.0
github.com/getsentry/sentry-go v0.43.0
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/cors v1.2.1
github.com/go-jose/go-jose/v4 v4.1.0
github.com/go-jose/go-jose/v4 v4.1.4
github.com/gobwas/ws v1.2.1
github.com/google/gopacket v1.1.19
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/json-iterator/go v1.1.12
github.com/mattn/go-colorable v0.1.13
github.com/miekg/dns v1.1.66
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/client_model v0.6.2
github.com/quic-go/quic-go v0.52.0
github.com/quic-go/quic-go v0.59.1
github.com/rs/zerolog v1.20.0
github.com/stretchr/testify v1.10.0
github.com/shirou/gopsutil/v4 v4.26.3
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.3.0
go.opentelemetry.io/contrib/propagators v0.22.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0
go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
go.opentelemetry.io/proto/otlp v1.2.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0
go.uber.org/automaxprocs v1.6.0
go.uber.org/mock v0.5.1
golang.org/x/crypto v0.38.0
golang.org/x/net v0.40.0
golang.org/x/sync v0.14.0
golang.org/x/sys v0.33.0
golang.org/x/term v0.32.0
google.golang.org/protobuf v1.36.6
go.uber.org/mock v0.5.2
golang.org/x/crypto v0.52.0
golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
google.golang.org/protobuf v1.36.11
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
nhooyr.io/websocket v1.8.7
@@ -50,54 +50,51 @@ require (
require (
github.com/BurntSushi/toml v1.2.0 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/grpc v1.72.2 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.81.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
@@ -108,5 +105,4 @@ replace github.com/prometheus/golang_client => github.com/prometheus/golang_clie
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
// This fork is based on quic-go v0.45
replace github.com/quic-go/quic-go => github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd
replace github.com/quic-go/quic-go => github.com/chungthuang/quic-go v0.45.1-0.20260529212404-a9fddf436fc4 // This fork is based on quic-go v0.59.1
+84 -89
View File
@@ -1,8 +1,6 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
@@ -11,18 +9,16 @@ github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd h1:VdYI5zFQ2h1/qzoC6rhyPx479bkF8i177Qpg4Q2n1vk=
github.com/chungthuang/quic-go v0.45.1-0.20250428085412-43229ad201fd/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/chungthuang/quic-go v0.45.1-0.20260529212404-a9fddf436fc4 h1:ZaFGQi6lUEnMyl0DvRy2mEp9u7FP+FrUBr7q+c4U68o=
github.com/chungthuang/quic-go v0.45.1-0.20260529212404-a9fddf436fc4/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/cloudflare/backoff v0.0.0-20240920015135-e46b80a3a7d0 h1:pRcxfaAlK0vR6nOeQs7eAEvjJzdGXl8+KaBlcvpQTyQ=
github.com/cloudflare/backoff v0.0.0-20240920015135-e46b80a3a7d0/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98 h1:c+Epklw9xk6BZ1OFBPWLA2PcL8QalKvl3if8CP9x8uw=
github.com/coredns/caddy v1.1.2-0.20241029205200-8de985351a98/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
github.com/coredns/coredns v1.12.2 h1:G4oDfi340zlVsriZ8nYiUemiQIew7nqOO+QPvPxIA4Y=
github.com/coredns/coredns v1.12.2/go.mod h1:GFz31oVOfCyMArFoypfu1SoaFoNkbdh6lDxtF1B6vfU=
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
@@ -33,6 +29,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg=
@@ -43,16 +41,14 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/getsentry/sentry-go v0.16.0 h1:owk+S+5XcgJLlGR/3+3s6N4d+uKwqYvh/eS0AIMjPWo=
github.com/getsentry/sentry-go v0.16.0/go.mod h1:ZXCloQLj0pG7mja5NK6NPf2V4A88YJ4pNlc2mOHwh6Y=
github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4=
github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
@@ -64,13 +60,15 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -81,8 +79,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
@@ -95,7 +91,6 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -107,18 +102,13 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34=
github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -138,16 +128,14 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -158,16 +146,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -176,6 +158,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
@@ -186,14 +170,16 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -204,10 +190,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
@@ -215,84 +205,89 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/propagators v0.22.0 h1:KGdv58M2//veiYLIhb31mofaI2LgkIPXXAZVeYVyfd8=
go.opentelemetry.io/contrib/propagators v0.22.0/go.mod h1:xGOuXr6lLIF9BXipA4pm6UuOSI0M98U6tsI3khbOiwU=
go.opentelemetry.io/otel v1.0.0-RC2/go.mod h1:w1thVQ7qbAy8MHb0IFj8a5Q2QU0l2ksf8u/CN8m3NOM=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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/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=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+1 -1
View File
@@ -43,7 +43,7 @@ func (tc *tcpConnection) Stream(_ context.Context, tunnelConn io.ReadWriter, _ *
func (tc *tcpConnection) Write(b []byte) (int, error) {
if tc.writeTimeout > 0 {
if err := tc.Conn.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
if err := tc.SetWriteDeadline(time.Now().Add(tc.writeTimeout)); err != nil {
tc.logger.Err(err).Msg("Error setting write deadline for TCP connection")
}
}
+23 -62
View File
@@ -13,7 +13,6 @@ import (
"time"
"github.com/gobwas/ws/wsutil"
gorillaWS "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/proxy"
@@ -61,7 +60,7 @@ func TestStreamTCPConnection(t *testing.T) {
})
errGroup.Go(func() error {
echoTCPOrigin(t, originConn)
originConn.Close()
_ = originConn.Close()
return nil
})
@@ -88,7 +87,7 @@ func TestDefaultStreamWSOverTCPConnection(t *testing.T) {
})
errGroup.Go(func() error {
echoTCPOrigin(t, originConn)
originConn.Close()
_ = originConn.Close()
return nil
})
@@ -117,14 +116,14 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
for _, status := range statusCodes {
handler := func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, []byte(sendMessage), body)
assert.NoError(t, err)
assert.Equal(t, []byte(sendMessage), body)
require.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
assert.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
w.Header().Set(echoHeaderName, echoHeaderReturnValue)
w.WriteHeader(status)
w.Write([]byte(echoMessage))
_, _ = w.Write([]byte(echoMessage))
}
origin := httptest.NewServer(http.HandlerFunc(handler))
defer origin.Close()
@@ -156,7 +155,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
errGroup.Go(func() error {
wsForwarderInConn, err := wsForwarderListener.Accept()
require.NoError(t, err)
defer wsForwarderInConn.Close()
defer func() { _ = wsForwarderInConn.Close() }()
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, TestLogger)
return nil
@@ -171,20 +170,22 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
// Request URL doesn't matter because the transport is using eyeballDialer to connectq
req, err := http.NewRequestWithContext(ctx, "GET", "http://test-socks-stream.com", bytes.NewBuffer([]byte(sendMessage)))
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = req.Body.Close() }()
req.Header.Set(echoHeaderName, echoHeaderIncomingValue)
resp, err := transport.RoundTrip(req)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, status, resp.StatusCode)
require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, []byte(echoMessage), body)
wsForwarderOutConn.Close()
edgeConn.Close()
tcpOverWSConn.Close()
_ = wsForwarderOutConn.Close()
_ = edgeConn.Close()
_ = tcpOverWSConn.Close()
require.NoError(t, errGroup.Wait())
}
@@ -205,7 +206,7 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
go func() {
time.Sleep(time.Millisecond * 10)
// Simulate losing connection to origin
originConn.Close()
_ = originConn.Close()
}()
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
tcpOverWSConn.Stream(ctx, eyeballConn, TestLogger)
@@ -221,11 +222,13 @@ func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
for i := 0; i < 50; i++ {
eyeballConn, edgeConn := net.Pipe()
req, err := http.NewRequestWithContext(ctx, http.MethodConnect, server.URL, edgeConn)
assert.NoError(t, err)
require.NoError(t, err)
defer func() { _ = req.Body.Close() }()
resp, err := client.Transport.RoundTrip(req)
assert.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
defer func() { _ = resp.Body.Close() }()
errGroup.Go(func() error {
for {
@@ -261,60 +264,18 @@ func echoWSEyeball(t *testing.T, conn net.Conn) {
assert.NoError(t, conn.Close())
}()
if !assert.NoError(t, wsutil.WriteClientBinary(conn, testMessage)) {
return
}
require.NoError(t, wsutil.WriteClientBinary(conn, testMessage))
readMsg, err := wsutil.ReadServerBinary(conn)
if !assert.NoError(t, err) {
return
}
require.NoError(t, err)
assert.Equal(t, testResponse, readMsg)
}
func echoWSOrigin(t *testing.T, expectMessages bool) *httptest.Server {
var upgrader = gorillaWS.Upgrader{
ReadBufferSize: 10,
WriteBufferSize: 10,
}
ws := func(w http.ResponseWriter, r *http.Request) {
header := make(http.Header)
for k, vs := range r.Header {
if k == "Test-Cloudflared-Echo" {
header[k] = vs
}
}
conn, err := upgrader.Upgrade(w, r, header)
require.NoError(t, err)
defer conn.Close()
sawMessage := false
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
if expectMessages && !sawMessage {
t.Errorf("unexpected error: %v", err)
}
return
}
assert.Equal(t, testMessage, p)
sawMessage = true
if err := conn.WriteMessage(messageType, testResponse); err != nil {
return
}
}
}
// NewTLSServer starts the server in another thread
return httptest.NewTLSServer(http.HandlerFunc(ws))
}
func echoTCPOrigin(t *testing.T, conn net.Conn) {
readBuffer := make([]byte, len(testMessage))
_, err := conn.Read(readBuffer)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, testMessage, readBuffer)
+6 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt"
"net"
"net/http"
_ "net/http/pprof"
_ "net/http/pprof" //nolint:gosec // G108: the sensitive /debug/pprof/cmdline endpoint is explicitly blocked in newMetricsHandler
"runtime"
"sync"
"time"
@@ -70,6 +70,11 @@ func newMetricsHandler(
log *zerolog.Logger,
) *http.ServeMux {
router := http.NewServeMux()
// Block /debug/pprof/cmdline to prevent leaking secret command-line arguments
// (e.g. tunnel tokens) that are exposed via os.Args.
router.HandleFunc("/debug/pprof/cmdline", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "forbidden", http.StatusForbidden)
})
router.Handle("/debug/", http.DefaultServeMux)
router.Handle("/metrics", promhttp.Handler())
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
+48
View File
@@ -0,0 +1,48 @@
package metrics
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/diagnostic"
)
func testHandler(t *testing.T) *http.ServeMux {
t.Helper()
log := zerolog.Nop()
return newMetricsHandler(Config{
DiagnosticHandler: diagnostic.NewDiagnosticHandler(
&log, 0, nil, uuid.Nil, uuid.Nil, nil, map[string]string{}, nil,
),
}, &log)
}
func TestPprofCmdlineEndpointIsBlocked(t *testing.T) {
t.Parallel()
handler := testHandler(t)
req := httptest.NewRequest(http.MethodGet, "/debug/pprof/cmdline", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestOtherPprofEndpointsStillWork(t *testing.T) {
t.Parallel()
handler := testHandler(t)
// /debug/pprof/ index should still be served by DefaultServeMux
req := httptest.NewRequest(http.MethodGet, "/debug/pprof/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
+427
View File
@@ -0,0 +1,427 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ../quic/quic_connection.go
//
// Generated by this command:
//
// mockgen -typed -build_flags=-tags=gomock -package mocks -destination mock_quic_connection.go -source=../quic/quic_connection.go
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
net "net"
reflect "reflect"
quic "github.com/quic-go/quic-go"
gomock "go.uber.org/mock/gomock"
)
// MockQUICConnection is a mock of QUICConnection interface.
type MockQUICConnection struct {
ctrl *gomock.Controller
recorder *MockQUICConnectionMockRecorder
isgomock struct{}
}
// MockQUICConnectionMockRecorder is the mock recorder for MockQUICConnection.
type MockQUICConnectionMockRecorder struct {
mock *MockQUICConnection
}
// NewMockQUICConnection creates a new mock instance.
func NewMockQUICConnection(ctrl *gomock.Controller) *MockQUICConnection {
mock := &MockQUICConnection{ctrl: ctrl}
mock.recorder = &MockQUICConnectionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockQUICConnection) EXPECT() *MockQUICConnectionMockRecorder {
return m.recorder
}
// AcceptStream mocks base method.
func (m *MockQUICConnection) AcceptStream(ctx context.Context) (*quic.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AcceptStream", ctx)
ret0, _ := ret[0].(*quic.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AcceptStream indicates an expected call of AcceptStream.
func (mr *MockQUICConnectionMockRecorder) AcceptStream(ctx any) *MockQUICConnectionAcceptStreamCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcceptStream", reflect.TypeOf((*MockQUICConnection)(nil).AcceptStream), ctx)
return &MockQUICConnectionAcceptStreamCall{Call: call}
}
// MockQUICConnectionAcceptStreamCall wrap *gomock.Call
type MockQUICConnectionAcceptStreamCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionAcceptStreamCall) Return(arg0 *quic.Stream, arg1 error) *MockQUICConnectionAcceptStreamCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionAcceptStreamCall) Do(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionAcceptStreamCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionAcceptStreamCall) DoAndReturn(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionAcceptStreamCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// CloseWithError mocks base method.
func (m *MockQUICConnection) CloseWithError(code quic.ApplicationErrorCode, reason string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CloseWithError", code, reason)
ret0, _ := ret[0].(error)
return ret0
}
// CloseWithError indicates an expected call of CloseWithError.
func (mr *MockQUICConnectionMockRecorder) CloseWithError(code, reason any) *MockQUICConnectionCloseWithErrorCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseWithError", reflect.TypeOf((*MockQUICConnection)(nil).CloseWithError), code, reason)
return &MockQUICConnectionCloseWithErrorCall{Call: call}
}
// MockQUICConnectionCloseWithErrorCall wrap *gomock.Call
type MockQUICConnectionCloseWithErrorCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionCloseWithErrorCall) Return(arg0 error) *MockQUICConnectionCloseWithErrorCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionCloseWithErrorCall) Do(f func(quic.ApplicationErrorCode, string) error) *MockQUICConnectionCloseWithErrorCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionCloseWithErrorCall) DoAndReturn(f func(quic.ApplicationErrorCode, string) error) *MockQUICConnectionCloseWithErrorCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ConnectionState mocks base method.
func (m *MockQUICConnection) ConnectionState() quic.ConnectionState {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConnectionState")
ret0, _ := ret[0].(quic.ConnectionState)
return ret0
}
// ConnectionState indicates an expected call of ConnectionState.
func (mr *MockQUICConnectionMockRecorder) ConnectionState() *MockQUICConnectionConnectionStateCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectionState", reflect.TypeOf((*MockQUICConnection)(nil).ConnectionState))
return &MockQUICConnectionConnectionStateCall{Call: call}
}
// MockQUICConnectionConnectionStateCall wrap *gomock.Call
type MockQUICConnectionConnectionStateCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionConnectionStateCall) Return(arg0 quic.ConnectionState) *MockQUICConnectionConnectionStateCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionConnectionStateCall) Do(f func() quic.ConnectionState) *MockQUICConnectionConnectionStateCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionConnectionStateCall) DoAndReturn(f func() quic.ConnectionState) *MockQUICConnectionConnectionStateCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Context mocks base method.
func (m *MockQUICConnection) Context() context.Context {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Context")
ret0, _ := ret[0].(context.Context)
return ret0
}
// Context indicates an expected call of Context.
func (mr *MockQUICConnectionMockRecorder) Context() *MockQUICConnectionContextCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockQUICConnection)(nil).Context))
return &MockQUICConnectionContextCall{Call: call}
}
// MockQUICConnectionContextCall wrap *gomock.Call
type MockQUICConnectionContextCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionContextCall) Return(arg0 context.Context) *MockQUICConnectionContextCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionContextCall) Do(f func() context.Context) *MockQUICConnectionContextCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionContextCall) DoAndReturn(f func() context.Context) *MockQUICConnectionContextCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// LocalAddr mocks base method.
func (m *MockQUICConnection) LocalAddr() net.Addr {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LocalAddr")
ret0, _ := ret[0].(net.Addr)
return ret0
}
// LocalAddr indicates an expected call of LocalAddr.
func (mr *MockQUICConnectionMockRecorder) LocalAddr() *MockQUICConnectionLocalAddrCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalAddr", reflect.TypeOf((*MockQUICConnection)(nil).LocalAddr))
return &MockQUICConnectionLocalAddrCall{Call: call}
}
// MockQUICConnectionLocalAddrCall wrap *gomock.Call
type MockQUICConnectionLocalAddrCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionLocalAddrCall) Return(arg0 net.Addr) *MockQUICConnectionLocalAddrCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionLocalAddrCall) Do(f func() net.Addr) *MockQUICConnectionLocalAddrCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionLocalAddrCall) DoAndReturn(f func() net.Addr) *MockQUICConnectionLocalAddrCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// OpenStream mocks base method.
func (m *MockQUICConnection) OpenStream() (*quic.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenStream")
ret0, _ := ret[0].(*quic.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// OpenStream indicates an expected call of OpenStream.
func (mr *MockQUICConnectionMockRecorder) OpenStream() *MockQUICConnectionOpenStreamCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStream", reflect.TypeOf((*MockQUICConnection)(nil).OpenStream))
return &MockQUICConnectionOpenStreamCall{Call: call}
}
// MockQUICConnectionOpenStreamCall wrap *gomock.Call
type MockQUICConnectionOpenStreamCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionOpenStreamCall) Return(arg0 *quic.Stream, arg1 error) *MockQUICConnectionOpenStreamCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionOpenStreamCall) Do(f func() (*quic.Stream, error)) *MockQUICConnectionOpenStreamCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionOpenStreamCall) DoAndReturn(f func() (*quic.Stream, error)) *MockQUICConnectionOpenStreamCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// OpenStreamSync mocks base method.
func (m *MockQUICConnection) OpenStreamSync(ctx context.Context) (*quic.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenStreamSync", ctx)
ret0, _ := ret[0].(*quic.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// OpenStreamSync indicates an expected call of OpenStreamSync.
func (mr *MockQUICConnectionMockRecorder) OpenStreamSync(ctx any) *MockQUICConnectionOpenStreamSyncCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenStreamSync", reflect.TypeOf((*MockQUICConnection)(nil).OpenStreamSync), ctx)
return &MockQUICConnectionOpenStreamSyncCall{Call: call}
}
// MockQUICConnectionOpenStreamSyncCall wrap *gomock.Call
type MockQUICConnectionOpenStreamSyncCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionOpenStreamSyncCall) Return(arg0 *quic.Stream, arg1 error) *MockQUICConnectionOpenStreamSyncCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionOpenStreamSyncCall) Do(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionOpenStreamSyncCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionOpenStreamSyncCall) DoAndReturn(f func(context.Context) (*quic.Stream, error)) *MockQUICConnectionOpenStreamSyncCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// ReceiveDatagram mocks base method.
func (m *MockQUICConnection) ReceiveDatagram(ctx context.Context) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReceiveDatagram", ctx)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReceiveDatagram indicates an expected call of ReceiveDatagram.
func (mr *MockQUICConnectionMockRecorder) ReceiveDatagram(ctx any) *MockQUICConnectionReceiveDatagramCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveDatagram", reflect.TypeOf((*MockQUICConnection)(nil).ReceiveDatagram), ctx)
return &MockQUICConnectionReceiveDatagramCall{Call: call}
}
// MockQUICConnectionReceiveDatagramCall wrap *gomock.Call
type MockQUICConnectionReceiveDatagramCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionReceiveDatagramCall) Return(arg0 []byte, arg1 error) *MockQUICConnectionReceiveDatagramCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionReceiveDatagramCall) Do(f func(context.Context) ([]byte, error)) *MockQUICConnectionReceiveDatagramCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionReceiveDatagramCall) DoAndReturn(f func(context.Context) ([]byte, error)) *MockQUICConnectionReceiveDatagramCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// RemoteAddr mocks base method.
func (m *MockQUICConnection) RemoteAddr() net.Addr {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoteAddr")
ret0, _ := ret[0].(net.Addr)
return ret0
}
// RemoteAddr indicates an expected call of RemoteAddr.
func (mr *MockQUICConnectionMockRecorder) RemoteAddr() *MockQUICConnectionRemoteAddrCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*MockQUICConnection)(nil).RemoteAddr))
return &MockQUICConnectionRemoteAddrCall{Call: call}
}
// MockQUICConnectionRemoteAddrCall wrap *gomock.Call
type MockQUICConnectionRemoteAddrCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionRemoteAddrCall) Return(arg0 net.Addr) *MockQUICConnectionRemoteAddrCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionRemoteAddrCall) Do(f func() net.Addr) *MockQUICConnectionRemoteAddrCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionRemoteAddrCall) DoAndReturn(f func() net.Addr) *MockQUICConnectionRemoteAddrCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// SendDatagram mocks base method.
func (m *MockQUICConnection) SendDatagram(payload []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SendDatagram", payload)
ret0, _ := ret[0].(error)
return ret0
}
// SendDatagram indicates an expected call of SendDatagram.
func (mr *MockQUICConnectionMockRecorder) SendDatagram(payload any) *MockQUICConnectionSendDatagramCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendDatagram", reflect.TypeOf((*MockQUICConnection)(nil).SendDatagram), payload)
return &MockQUICConnectionSendDatagramCall{Call: call}
}
// MockQUICConnectionSendDatagramCall wrap *gomock.Call
type MockQUICConnectionSendDatagramCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockQUICConnectionSendDatagramCall) Return(arg0 error) *MockQUICConnectionSendDatagramCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockQUICConnectionSendDatagramCall) Do(f func([]byte) error) *MockQUICConnectionSendDatagramCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockQUICConnectionSendDatagramCall) DoAndReturn(f func([]byte) error) *MockQUICConnectionSendDatagramCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
+279
View File
@@ -0,0 +1,279 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ../prechecks/resolvers.go
//
// Generated by this command:
//
// mockgen -typed -build_flags=-tags=gomock -package mocks -destination mock_resolvers.go -source=../prechecks/resolvers.go
//
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
tls "crypto/tls"
net "net"
netip "net/netip"
reflect "reflect"
time "time"
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.
type MockDNSResolver struct {
ctrl *gomock.Controller
recorder *MockDNSResolverMockRecorder
isgomock struct{}
}
// MockDNSResolverMockRecorder is the mock recorder for MockDNSResolver.
type MockDNSResolverMockRecorder struct {
mock *MockDNSResolver
}
// NewMockDNSResolver creates a new mock instance.
func NewMockDNSResolver(ctrl *gomock.Controller) *MockDNSResolver {
mock := &MockDNSResolver{ctrl: ctrl}
mock.recorder = &MockDNSResolverMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDNSResolver) EXPECT() *MockDNSResolverMockRecorder {
return m.recorder
}
// Resolve mocks base method.
func (m *MockDNSResolver) Resolve(region string) ([][]*allregions.EdgeAddr, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Resolve", region)
ret0, _ := ret[0].([][]*allregions.EdgeAddr)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Resolve indicates an expected call of Resolve.
func (mr *MockDNSResolverMockRecorder) Resolve(region any) *MockDNSResolverResolveCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockDNSResolver)(nil).Resolve), region)
return &MockDNSResolverResolveCall{Call: call}
}
// MockDNSResolverResolveCall wrap *gomock.Call
type MockDNSResolverResolveCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockDNSResolverResolveCall) Return(arg0 [][]*allregions.EdgeAddr, arg1 error) *MockDNSResolverResolveCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockDNSResolverResolveCall) Do(f func(string) ([][]*allregions.EdgeAddr, error)) *MockDNSResolverResolveCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockDNSResolverResolveCall) DoAndReturn(f func(string) ([][]*allregions.EdgeAddr, error)) *MockDNSResolverResolveCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockTCPDialer is a mock of TCPDialer interface.
type MockTCPDialer struct {
ctrl *gomock.Controller
recorder *MockTCPDialerMockRecorder
isgomock struct{}
}
// MockTCPDialerMockRecorder is the mock recorder for MockTCPDialer.
type MockTCPDialerMockRecorder struct {
mock *MockTCPDialer
}
// NewMockTCPDialer creates a new mock instance.
func NewMockTCPDialer(ctrl *gomock.Controller) *MockTCPDialer {
mock := &MockTCPDialer{ctrl: ctrl}
mock.recorder = &MockTCPDialerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTCPDialer) EXPECT() *MockTCPDialerMockRecorder {
return m.recorder
}
// DialEdge mocks base method.
func (m *MockTCPDialer) DialEdge(ctx context.Context, timeout time.Duration, tlsConfig *tls.Config, addr *net.TCPAddr, localIP net.IP) (net.Conn, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DialEdge", ctx, timeout, tlsConfig, addr, localIP)
ret0, _ := ret[0].(net.Conn)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DialEdge indicates an expected call of DialEdge.
func (mr *MockTCPDialerMockRecorder) DialEdge(ctx, timeout, tlsConfig, addr, localIP any) *MockTCPDialerDialEdgeCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialEdge", reflect.TypeOf((*MockTCPDialer)(nil).DialEdge), ctx, timeout, tlsConfig, addr, localIP)
return &MockTCPDialerDialEdgeCall{Call: call}
}
// MockTCPDialerDialEdgeCall wrap *gomock.Call
type MockTCPDialerDialEdgeCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockTCPDialerDialEdgeCall) Return(arg0 net.Conn, arg1 error) *MockTCPDialerDialEdgeCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockTCPDialerDialEdgeCall) Do(f func(context.Context, time.Duration, *tls.Config, *net.TCPAddr, net.IP) (net.Conn, error)) *MockTCPDialerDialEdgeCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockTCPDialerDialEdgeCall) DoAndReturn(f func(context.Context, time.Duration, *tls.Config, *net.TCPAddr, net.IP) (net.Conn, error)) *MockTCPDialerDialEdgeCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// MockQUICDialer is a mock of QUICDialer interface.
type MockQUICDialer struct {
ctrl *gomock.Controller
recorder *MockQUICDialerMockRecorder
isgomock struct{}
}
// MockQUICDialerMockRecorder is the mock recorder for MockQUICDialer.
type MockQUICDialerMockRecorder struct {
mock *MockQUICDialer
}
// NewMockQUICDialer creates a new mock instance.
func NewMockQUICDialer(ctrl *gomock.Controller) *MockQUICDialer {
mock := &MockQUICDialer{ctrl: ctrl}
mock.recorder = &MockQUICDialerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockQUICDialer) EXPECT() *MockQUICDialerMockRecorder {
return m.recorder
}
// DialQuic mocks base method.
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.QUICConnection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DialQuic indicates an expected call of DialQuic.
func (mr *MockQUICDialerMockRecorder) DialQuic(ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts any) *MockQUICDialerDialQuicCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialQuic", reflect.TypeOf((*MockQUICDialer)(nil).DialQuic), ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
return &MockQUICDialerDialQuicCall{Call: call}
}
// MockQUICDialerDialQuicCall wrap *gomock.Call
type MockQUICDialerDialQuicCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
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, *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, *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
}
// MockManagementDialer is a mock of ManagementDialer interface.
type MockManagementDialer struct {
ctrl *gomock.Controller
recorder *MockManagementDialerMockRecorder
isgomock struct{}
}
// MockManagementDialerMockRecorder is the mock recorder for MockManagementDialer.
type MockManagementDialerMockRecorder struct {
mock *MockManagementDialer
}
// NewMockManagementDialer creates a new mock instance.
func NewMockManagementDialer(ctrl *gomock.Controller) *MockManagementDialer {
mock := &MockManagementDialer{ctrl: ctrl}
mock.recorder = &MockManagementDialerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockManagementDialer) EXPECT() *MockManagementDialerMockRecorder {
return m.recorder
}
// DialContext mocks base method.
func (m *MockManagementDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DialContext", ctx, network, addr)
ret0, _ := ret[0].(net.Conn)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DialContext indicates an expected call of DialContext.
func (mr *MockManagementDialerMockRecorder) DialContext(ctx, network, addr any) *MockManagementDialerDialContextCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialContext", reflect.TypeOf((*MockManagementDialer)(nil).DialContext), ctx, network, addr)
return &MockManagementDialerDialContextCall{Call: call}
}
// MockManagementDialerDialContextCall wrap *gomock.Call
type MockManagementDialerDialContextCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockManagementDialerDialContextCall) Return(arg0 net.Conn, arg1 error) *MockManagementDialerDialContextCall {
c.Call = c.Call.Return(arg0, arg1)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockManagementDialerDialContextCall) Do(f func(context.Context, string, string) (net.Conn, error)) *MockManagementDialerDialContextCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockManagementDialerDialContextCall) DoAndReturn(f func(context.Context, string, string) (net.Conn, error)) *MockManagementDialerDialContextCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
+4
View File
@@ -3,3 +3,7 @@
package mocks
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package mocks -destination mock_limiter.go -source=../flow/limiter.go Limiter"
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package mocks -destination mock_resolvers.go -source=../prechecks/resolvers.go"
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package mocks -destination mock_quic_connection.go -source=../quic/quic_connection.go"
+2 -2
View File
@@ -28,7 +28,7 @@ func FindProtocol(p []byte) (layers.IPProtocol, error) {
// Next header is in the 7th byte of IPv6 header
return layers.IPProtocol(p[6]), nil
default:
return 0, fmt.Errorf("unknow ip version %d", version)
return 0, fmt.Errorf("unknown ip version %d", version)
}
}
@@ -105,7 +105,7 @@ func (pd *IPDecoder) decodeByVersion(packet []byte) ([]gopacket.LayerType, error
case 6:
err = pd.v6parser.DecodeLayers(packet, &decoded)
default:
err = fmt.Errorf("unknow ip version %d", version)
err = fmt.Errorf("unknown ip version %d", version)
}
if err != nil {
return nil, err
+383
View File
@@ -0,0 +1,383 @@
package prechecks
import (
"context"
"fmt"
"slices"
"time"
"github.com/cloudflare/backoff"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
)
const (
defaultTimeout = 10 * time.Second
maxRetries = 2
retryBaseDelay = 1 * time.Second
maxRetryDelay = 16 * time.Second
)
// RunDialers holds the injectable dependencies for Run(). Production callers build
// this with real implementations; tests supply mocks.
type RunDialers struct {
DNSResolver DNSResolver
TCPDialer TCPDialer
QUICDialer QUICDialer
ManagementDialer ManagementDialer
}
// TransportResults holds the per-target results for each transport probe type.
// Each slice has one entry per resolved target group, in the same order as the
// target labels slice.
type TransportResults struct {
QUIC []CheckResult // one per target group
HTTP2 []CheckResult // one per target group
ManagementAPI CheckResult // single target, no groups
}
// Collect returns all results as a slice in a consistent order for reporting:
// all QUIC rows first (one per target), then all HTTP2 rows, then Management API.
func (tr TransportResults) Collect() []CheckResult {
results := make([]CheckResult, 0, len(tr.QUIC)+len(tr.HTTP2)+1)
results = append(results, tr.QUIC...)
results = append(results, tr.HTTP2...)
results = append(results, tr.ManagementAPI)
return results
}
// Run executes the following connectivity pre-checks:
//
// 1. Edge address resolution — either DNS-based SRV discovery (normal path)
// or direct resolution of --edge addresses (static path). The static path
// skips DNS probe rows entirely since there are no SRV records to validate.
// 2. QUIC, HTTP/2, and Management API probes run concurrently against the
// resolved addresses.
//
// Each failed probe is retried up to maxRetries times with exponential backoff.
// The suite is bounded by cfg.Timeout (defaultTimeout if zero).
func Run(ctx context.Context, caCert string, cfg Config, log *zerolog.Logger, runDialers RunDialers) Report {
runID := uuid.New()
if cfg.Timeout <= 0 {
cfg.Timeout = defaultTimeout
}
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)
// 1) Resolve edge addresses. Each ResolvedTarget bundles its addr group
// with the DNS CheckResult that labels it, keeping the two in sync.
var resolvedTargets []ResolvedTarget
if len(cfg.EdgeAddrs) > 0 {
// Static path: explicit --edge addresses, one ResolvedTarget per addr.
resolvedTargets = resolveStaticEdge(cfg.EdgeAddrs, log)
} else {
// Normal path: SRV-based discovery; DNS rows carry Pass or Fail status.
resolvedTargets = runDNSProbe(ctx, runDialers.DNSResolver, cfg.Region)
}
// Extract parallel slices for the transport probe layer.
// nolint:prealloc // False positive. The linter is confused by the append used when producing Report.Results
dnsResults := make([]CheckResult, len(resolvedTargets))
perGroupAddrs := make([][]*allregions.EdgeAddr, len(resolvedTargets))
targetLabels := make([]string, len(resolvedTargets))
for i, rt := range resolvedTargets {
dnsResults[i] = rt.DNSResult
perGroupAddrs[i] = rt.Addrs
targetLabels[i] = rt.DNSResult.Target
}
// dnsOK is true when at least one target has addresses to probe.
dnsOK := slices.ContainsFunc(resolvedTargets, func(r ResolvedTarget) bool {
return len(r.Addrs) > 0
})
// 2) Run transport probes concurrently. Each probe type gets its own
// buffered channel — one send, one receive, no routing required.
var results TransportResults
mgmtCh := make(chan CheckResult)
go func() {
mgmtCh <- probeManagementAPIWithRetry(ctx, runDialers.ManagementDialer)
}()
if !dnsOK {
// No addresses available: emit one skip row per target so the table
// stays consistent with the DNS rows above.
results.QUIC = skipResultsForTargets(dnsResults, ProbeTypeQUIC, componentUDPConnectivity)
results.HTTP2 = skipResultsForTargets(dnsResults, ProbeTypeHTTP2, componentTCPConnectivity)
} else {
filteredAddrs := addrsByGroup(perGroupAddrs, cfg.IPVersion)
quicCh := make(chan []CheckResult, 1)
http2Ch := make(chan []CheckResult, 1)
go func() {
if quicTLSErr != nil {
log.Warn().Err(quicTLSErr).Msg("Failed to build QUIC probe TLS config")
quicCh <- tlsConfigErrResults(ProbeTypeQUIC, componentUDPConnectivity,
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, quicTLSErr), actionQUICBlocked)
return
}
quicCh <- probeAllTargets(ctx, ProbeTypeQUIC, componentUDPConnectivity,
filteredAddrs, targetLabels,
func(addr *allregions.EdgeAddr) CheckResult {
return probeQUIC(ctx, quicTLSConfig, runDialers.QUICDialer, addr, log)
})
}()
go func() {
if http2TLSErr != nil {
log.Warn().Err(http2TLSErr).Msg("Failed to build HTTP/2 probe TLS config")
http2Ch <- tlsConfigErrResults(ProbeTypeHTTP2, componentTCPConnectivity,
targetLabels, fmt.Sprintf("%s: %v", detailsTLSConfigFailed, http2TLSErr), actionHTTP2Blocked)
return
}
http2Ch <- probeAllTargets(ctx, ProbeTypeHTTP2, componentTCPConnectivity,
filteredAddrs, targetLabels,
func(addr *allregions.EdgeAddr) CheckResult {
return probeHTTP2(ctx, http2TLSConfig, runDialers.TCPDialer, addr)
})
}()
results.QUIC = <-quicCh
results.HTTP2 = <-http2Ch
}
results.ManagementAPI = <-mgmtCh
return Report{
RunID: runID,
Results: append(dnsResults, results.Collect()...),
SuggestedProtocol: suggestProtocol(results.QUIC, results.HTTP2, cfg.ProtocolOverride),
}
}
// tlsConfigErrResults returns one Fail CheckResult per target, used when
// TLS config construction fails before any dial is attempted.
func tlsConfigErrResults(probeType ProbeType, component string, targets []string, details, action string) []CheckResult {
results := make([]CheckResult, len(targets))
for i, target := range targets {
results[i] = CheckResult{
Type: probeType,
Component: component,
Target: target,
ProbeStatus: Fail,
Details: details,
Action: action,
}
}
return results
}
// probeAllTargets probes each target group sequentially and returns one
// CheckResult per group. Within each group, all available addresses (V4 and/or
// V6) are tried and the best result is kept.
func probeAllTargets(
ctx context.Context,
probeType ProbeType,
component string,
perGroupAddrs [][]*allregions.EdgeAddr,
targets []string,
probeFn func(*allregions.EdgeAddr) CheckResult,
) []CheckResult {
results := make([]CheckResult, len(perGroupAddrs))
for i, addrs := range perGroupAddrs {
results[i] = probeTarget(ctx, probeType, component, targets[i], addrs, probeFn)
}
return results
}
// probeTarget probes all addresses for a single target group (typically one V4
// and/or one V6) and returns the best result. Any address passing means the
// target is reachable, so Pass beats Fail within a group.
func probeTarget(
ctx context.Context,
probeType ProbeType,
component string,
target string,
addrs []*allregions.EdgeAddr,
probeFn func(*allregions.EdgeAddr) CheckResult,
) CheckResult {
if len(addrs) == 0 {
return CheckResult{
Type: probeType,
Component: component,
Target: target,
ProbeStatus: Skip,
Details: "No suitable address found for configured IP version",
}
}
best := probeWithRetry(ctx, addrs[0], probeFn)
for _, addr := range addrs[1:] {
if r := probeWithRetry(ctx, addr, probeFn); r.ProbeStatus == Pass {
best = r
}
}
best.Target = target
return best
}
// probeManagementAPIWithRetry runs the Cloudflare API reachability probe with retry.
func probeManagementAPIWithRetry(ctx context.Context, dialer ManagementDialer) CheckResult {
var r CheckResult
withRetry(ctx, maxRetries, func() bool {
r = probeManagementAPI(ctx, dialer)
return r.ProbeStatus == Pass
})
return r
}
// probeWithRetry calls probeFn on addr with exponential-backoff retry up to
// maxRetries times, stopping as soon as the probe passes.
func probeWithRetry(ctx context.Context, addr *allregions.EdgeAddr, probeFn func(*allregions.EdgeAddr) CheckResult) CheckResult {
var r CheckResult
withRetry(ctx, maxRetries, func() bool {
r = probeFn(addr)
return r.ProbeStatus == Pass
})
return r
}
// addrsByGroup returns the addresses to probe for each resolved target group,
// preserving the per-group structure. Each inner slice contains at most one V4
// and one V6 address (subject to ipVersion).
func addrsByGroup(addrGroups [][]*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) [][]*allregions.EdgeAddr {
perGroup := make([][]*allregions.EdgeAddr, 0, len(addrGroups))
for _, group := range addrGroups {
v4, v6 := addrsByFamily(group, ipVersion)
var addrs []*allregions.EdgeAddr
if v4 != nil {
addrs = append(addrs, v4)
}
if v6 != nil {
addrs = append(addrs, v6)
}
perGroup = append(perGroup, addrs)
}
return perGroup
}
// skipResultsForTargets returns one skip CheckResult per entry in results,
// using each entry's Target label so the transport row aligns with its DNS row.
func skipResultsForTargets(targets []CheckResult, probeType ProbeType, component string) []CheckResult {
results := make([]CheckResult, len(targets))
for i, t := range targets {
results[i] = skipResult(probeType, component, t.Target, detailsDNSPrerequisiteFailed)
}
return results
}
// worstStatus returns the most severe Status across a slice of CheckResults.
// Fail > Pass > Skip. Used to determine whether a transport type as a whole
// should be considered failed (any region failing = transport fails).
func worstStatus(results []CheckResult) Status {
worst := Skip
for _, r := range results {
if severity(r.ProbeStatus) > severity(worst) {
worst = r.ProbeStatus
}
}
return worst
}
// severity maps a Status to a comparable integer so that worse outcomes rank higher.
func severity(s Status) int {
switch s {
case Fail:
return 2
case Pass:
return 1
case Skip:
return 0
default:
return 0
}
}
// 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, 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
}
if len(http2Results) > 0 && worstStatus(http2Results) == Pass {
http2 := connection.HTTP2
return &http2
}
return nil
}
// withRetry calls fn up to 1+maxAttempts times, stopping as soon as fn returns
// true. Between attempts, it sleeps with exponential backoff bounded by
// maxRetryDelay, and stops early if ctx is done.
func withRetry(ctx context.Context, maxAttempts int, fn func() bool) {
b := backoff.NewWithoutJitter(maxRetryDelay, retryBaseDelay)
for attempt := 0; attempt <= maxAttempts; attempt++ {
if fn() {
return
}
if attempt == maxAttempts {
break
}
timer := time.NewTimer(b.Duration())
select {
case <-ctx.Done():
timer.Stop()
return
case <-timer.C:
}
}
}
+730
View File
@@ -0,0 +1,730 @@
package prechecks
import (
"errors"
"math"
"net"
"testing"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/mocks"
)
const (
emptyCert = ""
)
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// twoRegionAddrs returns a two-group [][]*EdgeAddr with one IPv4 address per
// region. Used by tests that only need to exercise the V4 path.
func twoRegionAddrs() [][]*allregions.EdgeAddr {
makeV4 := func(ip string) *allregions.EdgeAddr {
parsed := net.ParseIP(ip)
return &allregions.EdgeAddr{
TCP: &net.TCPAddr{IP: parsed, Port: 7844},
UDP: &net.UDPAddr{IP: parsed, Port: 7844},
IPVersion: allregions.V4,
}
}
return [][]*allregions.EdgeAddr{
{makeV4("1.2.3.4")},
{makeV4("5.6.7.8")},
}
}
// twoRegionAddrsBothFamilies returns a two-group [][]*EdgeAddr with one IPv4
// and one IPv6 address per region, used by per-family probe tests.
func twoRegionAddrsBothFamilies() [][]*allregions.EdgeAddr {
makeAddr := func(ip string, v allregions.EdgeIPVersion) *allregions.EdgeAddr {
parsed := net.ParseIP(ip)
return &allregions.EdgeAddr{
TCP: &net.TCPAddr{IP: parsed, Port: 7844},
UDP: &net.UDPAddr{IP: parsed, Port: 7844},
IPVersion: v,
}
}
return [][]*allregions.EdgeAddr{
{makeAddr("1.2.3.4", allregions.V4), makeAddr("2001:db8::1", allregions.V6)},
{makeAddr("5.6.7.8", allregions.V4), makeAddr("2001:db8::2", allregions.V6)},
}
}
// nopConn is a net.Conn whose Close() is a no-op, used as the success value
// for TCP and management dial mocks.
type nopConn struct{ net.Conn }
func (nopConn) Close() error { return nil }
// requireStatuses asserts the probe statuses in report.Results match
// expected (in order), failing immediately on length mismatch.
func requireStatuses(t *testing.T, report Report, expected ...Status) {
t.Helper()
require.Len(t, report.Results, len(expected))
for i, want := range expected {
got := report.Results[i].ProbeStatus
assert.Equalf(t, want, got,
"result[%d] (%s/%s): got %s, want %s",
i, report.Results[i].Component, report.Results[i].Target, got, want)
}
}
func nopLogger() *zerolog.Logger {
l := zerolog.Nop()
return &l
}
// newFakeQUICConn creates a mock QUIC connection with CloseWithError
// expectation pre-configured so gomock does not fail at runtime.
func newFakeQUICConn(ctrl *gomock.Controller) *mocks.MockQUICConnection {
conn := mocks.NewMockQUICConnection(ctrl)
conn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
return conn
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
// TestRun_AllPass verifies that when all probes succeed the report contains
// 7 rows: 2 DNS + 2 QUIC (one per region) + 2 HTTP/2 (one per region) + 1 API.
func TestRun_AllPass(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)
fakeQUICConn := newFakeQUICConn(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
// twoRegionAddrs has 2 regions × 1 V4 address each = 2 dials per transport.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
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)
assert.NotEqual(t, uuid.Nil, report.RunID, "RunID must be set")
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
assert.False(t, report.hasWarn())
}
// TestRun_QUICBlocked verifies that when QUIC is blocked on all regions,
// the report is degraded (warn) and HTTP/2 is the suggested protocol.
func TestRun_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("connection refused")).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
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)
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
assert.True(t, report.hasWarn())
}
// TestRun_HTTP2Blocked verifies that when HTTP/2 is blocked on all regions,
// the report is degraded (warn) and QUIC is the suggested protocol.
func TestRun_HTTP2Blocked(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
fakeQUICConn := newFakeQUICConn(ctrl)
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("connection refused")).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)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
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)
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
assert.True(t, report.hasWarn())
}
// TestRun_BothTransportsBlocked verifies that when both QUIC and HTTP/2 are
// blocked on all regions it is a hard fail with no suggested protocol.
func TestRun_BothTransportsBlocked(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)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
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)
assert.Nil(t, report.SuggestedProtocol)
assert.True(t, report.hasHardFail())
}
// TestRun_PartialRegionQUICFail verifies "worst wins" semantics: when QUIC
// passes for region1 but fails for region2, QUIC is treated as failed and
// HTTP/2 becomes the suggested protocol.
func TestRun_PartialRegionQUICFail(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)
fakeQUICConn := newFakeQUICConn(ctrl)
// Two regions: 1.2.3.4 (region1) and 5.6.7.8 (region2).
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
// TCP/HTTP2: both regions pass.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).AnyTimes()
// QUIC: region1 (1.2.3.4) passes, region2 (5.6.7.8) fails.
region1Addr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 7844}
region2Addr := &net.UDPAddr{IP: net.ParseIP("5.6.7.8"), Port: 7844}
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), region1Addr.AddrPort(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).AnyTimes()
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), region2Addr.AddrPort(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, errors.New("connection refused")).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
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)
// Worst wins: region2 QUIC failed, so QUIC is treated as failed overall.
// HTTP/2 passes on all regions → HTTP/2 is the suggested protocol.
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
assert.True(t, report.hasWarn())
}
// TestRun_DNSFail_SkipsTransports verifies that when DNS fails, transport rows
// are emitted as Skip (one per DNS region) and no transport dials are made.
func TestRun_DNSFail_SkipsTransports(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(nil, errors.New("no such host")).AnyTimes()
// Transport dialers must NOT be called when DNS fails.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
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.
// 2 DNS Fail + 2 QUIC Skip + 2 HTTP2 Skip + 1 API Pass = 7 results.
require.Len(t, report.Results, 7)
assert.Equal(t, Fail, report.Results[0].ProbeStatus, "DNS region1")
assert.Equal(t, Fail, report.Results[1].ProbeStatus, "DNS region2")
assert.Equal(t, Skip, report.Results[2].ProbeStatus, "QUIC region1 must be skipped")
assert.Equal(t, Skip, report.Results[3].ProbeStatus, "QUIC region2 must be skipped")
assert.Equal(t, Skip, report.Results[4].ProbeStatus, "HTTP/2 region1 must be skipped")
assert.Equal(t, Skip, report.Results[5].ProbeStatus, "HTTP/2 region2 must be skipped")
assert.Equal(t, Pass, report.Results[6].ProbeStatus, "API still runs")
assert.True(t, report.hasHardFail())
}
// TestRun_ManagementAPIFail verifies that a Management API failure results
// in a warning (not a hard fail) and QUIC remains the suggested protocol.
func TestRun_ManagementAPIFail(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)
fakeQUICConn := newFakeQUICConn(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
// twoRegionAddrs has 2 regions × 1 V4 address each; each succeeds on first try.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(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})
// 2 DNS Pass + 2 QUIC Pass + 2 HTTP2 Pass + 1 API Fail.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Fail)
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
assert.True(t, report.hasWarn())
}
// TestRun_RegionFlagForwardedToDNS verifies that the --region flag is passed
// verbatim to the DNS resolver and that regional hostnames appear in the results.
func TestRun_RegionFlagForwardedToDNS(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)
fakeQUICConn := newFakeQUICConn(ctrl)
// The region string must be forwarded verbatim to the DNS resolver.
dns.EXPECT().Resolve("us").Return(twoRegionAddrs(), nil)
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Region: "us", Timeout: 2 * time.Second, IPVersion: allregions.Auto},
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")
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[1].Target, "DNS region2")
// Transport rows reuse the same regional hostnames (QUIC: 2,3 / HTTP2: 4,5).
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[2].Target, "QUIC region1")
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[3].Target, "QUIC region2")
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[4].Target, "HTTP2 region1")
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[5].Target, "HTTP2 region2")
}
// TestRun_QUICUsesProbeConnIndex verifies that the QUIC probe always uses the
// reserved sentinel connIndex (math.MaxUint8 = 255) to bypass port-reuse checks.
func TestRun_QUICUsesProbeConnIndex(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)
fakeQUICConn := newFakeQUICConn(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).Times(2)
// connIndex must be the reserved sentinel (math.MaxUint8 = 255), never 0.
// twoRegionAddrs has 2 regions × 1 V4 address each → 2 calls.
quicD.EXPECT().DialQuic(
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Eq(uint8(math.MaxUint8)),
gomock.Any(), gomock.Any(),
).Return(fakeQUICConn, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
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})
}
// TestRun_BothFamiliesProbed verifies that when both V4 and V6 addresses are
// present in the DNS response, both are probed (2 regions × 2 families = 4 dials).
func TestRun_BothFamiliesProbed(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)
fakeQUICConn := newFakeQUICConn(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
// 2 regions × 2 families = 4 dial calls each for QUIC and HTTP/2.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(4)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).Times(4)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
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})
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results, all passing.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
}
// TestRun_IPVersionRestriction verifies that when a single IP family is
// configured, only that family is probed (2 regions × 1 addr = 2 dials per
// transport) and the excluded family is never dialled.
func TestRun_IPVersionRestriction(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ipVersion allregions.ConfigIPVersion
}{
{"IPv4Only skips V6", allregions.IPv4Only},
{"IPv6Only skips V4", allregions.IPv6Only},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
dns := mocks.NewMockDNSResolver(ctrl)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
fakeQUICConn := newFakeQUICConn(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
// 2 regions × 1 addr per restricted family = 2 dials each.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: tt.ipVersion},
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
})
}
}
// TestRun_EdgeAddrs_SingleAddr verifies that a single --edge addr bypasses DNS
// probing. The report contains one DNS Skip row, transport rows labeled with
// the raw addr string, and the Management API row.
func TestRun_EdgeAddrs_SingleAddr(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
fakeQUICConn := newFakeQUICConn(ctrl)
// DNS resolver must NOT be called when EdgeAddrs is set.
dns := mocks.NewMockDNSResolver(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Times(0)
// One addr resolves to one group → one dial per transport.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(1)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).Times(1)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
EdgeAddrs: []string{"127.0.0.1:7844"},
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 1 DNS Skip + 1 QUIC + 1 HTTP2 + 1 API = 4 results.
requireStatuses(t, report, Pass, Pass, Pass, Pass)
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip")
assert.Equal(t, "127.0.0.1:7844", report.Results[1].Target, "QUIC target must be the raw --edge addr")
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "HTTP2 target must be the raw --edge addr")
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
}
// TestRun_EdgeAddrs_MultipleAddrs verifies that multiple --edge addrs produce
// one transport row per addr, each labeled with its original addr string.
func TestRun_EdgeAddrs_MultipleAddrs(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
fakeQUICConn := newFakeQUICConn(ctrl)
dns := mocks.NewMockDNSResolver(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Times(0)
// Two addrs → two groups → two dials per transport.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil).Times(2)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(fakeQUICConn, nil).Times(2)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
EdgeAddrs: []string{"127.0.0.1:7844", "127.0.0.2:7844"},
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 2 DNS Pass (one per addr) + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip addr1")
assert.Equal(t, "127.0.0.1:7844", report.Results[0].Target, "DNS skip addr1 label")
assert.Equal(t, ProbeTypeDNS, report.Results[1].Type, "second row must be DNS skip addr2")
assert.Equal(t, "127.0.0.2:7844", report.Results[1].Target, "DNS skip addr2 label")
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "QUIC addr1")
assert.Equal(t, "127.0.0.2:7844", report.Results[3].Target, "QUIC addr2")
assert.Equal(t, "127.0.0.1:7844", report.Results[4].Target, "HTTP2 addr1")
assert.Equal(t, "127.0.0.2:7844", report.Results[5].Target, "HTTP2 addr2")
}
// TestRun_EdgeAddrs_UnresolvableAddr verifies that when all --edge addrs fail
// to resolve, the DNS resolver is not called and transport rows are skipped,
// mirroring the DNS skip row.
func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns := mocks.NewMockDNSResolver(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Times(0)
// Unresolvable addr → no groups → no transport dials.
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
EdgeAddrs: []string{"not-a-valid-addr"},
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// 1 DNS Fail + 1 QUIC Skip + 1 HTTP2 Skip + 1 API = 4 results.
requireStatuses(t, report, Fail, Skip, Skip, Pass)
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type)
assert.Equal(t, "not-a-valid-addr", report.Results[0].Target)
assert.Equal(t, ProbeTypeQUIC, report.Results[1].Type)
assert.Equal(t, ProbeTypeHTTP2, report.Results[2].Type)
assert.Nil(t, report.SuggestedProtocol)
assert.True(t, report.hasHardFail())
}
// ---------------------------------------------------------------------------
// Protocol override tests
// ---------------------------------------------------------------------------
// TestRun_ProtocolOverride_HTTP2_BothPass verifies that when --protocol http2
// is set and both transports are reachable, the summary reports HTTP/2 (not
// QUIC, which would otherwise win the heuristic).
func TestRun_ProtocolOverride_HTTP2_BothPass(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
dns := mocks.NewMockDNSResolver(ctrl)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
fakeQUICConn := newFakeQUICConn(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, 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)
fakeQUICConn := newFakeQUICConn(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, 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, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
require.NotNil(t, report.SuggestedProtocol)
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
assert.False(t, report.hasHardFail())
}
// TestRun_ProtocolOverride_HTTP2_BothBlocked verifies that when --protocol http2
// is set but the HTTP/2 transport itself also fails (hard fail), the override
// falls through to the heuristic which returns nil — there is no usable protocol.
func TestRun_ProtocolOverride_HTTP2_BothBlocked(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
dns := mocks.NewMockDNSResolver(ctrl)
tcp := mocks.NewMockTCPDialer(ctrl)
quicD := mocks.NewMockQUICDialer(ctrl)
mgmt := mocks.NewMockManagementDialer(ctrl)
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, errors.New("blocked")).AnyTimes()
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, errors.New("blocked")).AnyTimes()
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nopConn{}, nil)
cfg := Config{
Timeout: 2 * time.Second,
IPVersion: allregions.Auto,
ProtocolOverride: "http2",
}
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
// The overridden transport (HTTP/2) is blocked, so the override cannot be
// honoured and the hard-fail path reports no suggested protocol.
assert.Nil(t, report.SuggestedProtocol)
assert.True(t, report.hasHardFail())
}
+376
View File
@@ -0,0 +1,376 @@
package prechecks
import (
"context"
"crypto/tls"
"fmt"
"math"
"net"
"net/netip"
"time"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection/dialopts"
"github.com/cloudflare/cloudflared/connection"
edgedial "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tlsconfig"
)
const (
perProbeDialTimeout = 5 * time.Second
// Action messages for each probe outcome.
actionDNSFail = "Ensure your DNS resolver can resolve '%s'. Run: dig A %s @1.1.1.1. If that fails, contact your network administrator."
actionQUICBlocked = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
actionHTTP2Blocked = "Allow outbound TCP on port 7844."
actionAPIUnreachable = "cloudflared will still run, but automatic software updates are unavailable. " +
"Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates."
// Component names for CheckResult.
componentDNSResolution = "DNS Resolution"
componentUDPConnectivity = "UDP Connectivity"
componentTCPConnectivity = "TCP Connectivity"
componentCloudflareAPI = "Cloudflare API"
// Target identifiers for CheckResult.
targetPortQUIC = "Port 7844 (QUIC)"
targetPortHTTP2 = "Port 7844 (HTTP/2)"
targetAPI = "api.cloudflare.com:443"
noDNSTarget = "No DNS target (Using edge flag)"
// Details messages for CheckResult.
dnsNoAddressesReturned = "No addresses returned"
dnsResolvedSuccessfully = "DNS Resolved successfully"
detailsQUICHandshakeFailed = "QUIC connection failed"
detailsQUICHandshakeSuccessful = "QUIC connection successful"
detailsHTTP2BlockedOrUnreachable = "HTTP/2 connection is blocked or unreachable"
detailsHTTP2HandshakeSuccessful = "HTTP/2 connection successful"
detailsAPIConnectionFailed = "API Connection failed"
detailsApiReachable = "API is reachable"
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
detailsTLSConfigFailed = "TLS configuration failed"
// Region hostname templates.
region1Global = "region1.v2.argotunnel.com"
region2Global = "region2.v2.argotunnel.com"
region1US = "us-region1.v2.argotunnel.com"
region2US = "us-region2.v2.argotunnel.com"
region1Fed = "fed-region1.v2.argotunnel.com"
region2Fed = "fed-region2.v2.argotunnel.com"
)
// EdgeDNSResolver implements DNSResolver for the standard DNS-based edge
// discovery path.
type EdgeDNSResolver struct {
Log *zerolog.Logger
}
func (r *EdgeDNSResolver) Resolve(region string) ([][]*allregions.EdgeAddr, error) {
return allregions.EdgeDiscovery(r.Log, allregions.RegionalServiceName(region))
}
type EdgeTCPDialer struct{}
func (d *EdgeTCPDialer) DialEdge(
ctx context.Context,
timeout time.Duration,
tlsConfig *tls.Config,
addr *net.TCPAddr,
localIP net.IP,
) (net.Conn, error) {
return edgedial.DialEdge(ctx, timeout, tlsConfig, addr, localIP)
}
type EdgeQUICDialer struct{}
func (d *EdgeQUICDialer) DialQuic(
ctx context.Context,
quicConfig *quic.Config,
tlsConfig *tls.Config,
addr netip.AddrPort,
localAddr net.IP,
connIndex uint8,
logger *zerolog.Logger,
opts dialopts.DialOpts,
) (cfdquic.QUICConnection, error) {
return connection.DialQuic(ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
}
type NetManagementDialer struct {
Dialer net.Dialer
}
func (d *NetManagementDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
return d.Dialer.DialContext(ctx, network, addr)
}
// 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) {
settings := p.ProbeTLSSettings()
if settings == nil {
return nil, fmt.Errorf("no probe TLS settings for protocol %s", p)
}
cfg, err := tlsconfig.CreateTunnelConfig(caCert, settings.ServerName)
if err != nil {
return nil, err
}
if len(settings.NextProtos) > 0 {
cfg.NextProtos = settings.NextProtos
}
return cfg, nil
}
// probeDNS resolves edge addresses for the given region via the supplied
// DNSResolver and returns one ResolvedTarget per discovered region. If
// resolution fails entirely, every ResolvedTarget will carry a Fail DNSResult
// and nil Addrs.
func probeDNS(
resolver DNSResolver,
region string,
) []ResolvedTarget {
region1Target, region2Target := regionTargets(region)
targets := []string{region1Target, region2Target}
addrGroups, err := resolver.Resolve(region)
if err != nil || len(addrGroups) == 0 {
detail := dnsNoAddressesReturned
if err != nil {
detail = err.Error()
}
return []ResolvedTarget{
{DNSResult: newDNSCheckResult(region1Target, Fail, detail, fmt.Sprintf(actionDNSFail, region1Target, region1Target))},
{DNSResult: newDNSCheckResult(region2Target, Fail, detail, fmt.Sprintf(actionDNSFail, region2Target, region2Target))},
}
}
resolved := make([]ResolvedTarget, 0, len(addrGroups))
for i, target := range targets {
if i >= len(addrGroups) {
break
}
group := addrGroups[i]
if len(group) == 0 {
resolved = append(resolved, ResolvedTarget{
DNSResult: newDNSCheckResult(target, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)),
})
} else {
resolved = append(resolved, ResolvedTarget{
Addrs: group,
DNSResult: newDNSCheckResult(target, Pass, dnsResolvedSuccessfully, ""),
})
}
}
return resolved
}
// probeQUIC performs a QUIC handshake to a single edge address and returns a
// CheckResult. The connection is closed immediately after the handshake no
// streams are opened and no RPC frames are sent to avoid triggering the OTD
// registration timeout (TUN-6732). The probe SNI (probe.cftunnel.com) is used
// instead of the production quic.cftunnel.com to prevent OTD log noise.
//
// A per-probe deadline (perProbeDialTimeout) is applied on top of the parent
// context so that a single blocked handshake cannot consume the entire suite
// budget.
func probeQUIC(
ctx context.Context,
tlsConfig *tls.Config,
dialer QUICDialer,
addr *allregions.EdgeAddr,
logger *zerolog.Logger,
) CheckResult {
dialCtx, cancel := context.WithTimeout(ctx, perProbeDialTimeout)
defer cancel()
// We call dialer.DialQuic with isProbe = true, which bypasses connIndex check.
// Therefore, whatever we add to connIndex will not be relevant.
edgeAddrPort := addr.UDP.AddrPort()
conn, err := dialer.DialQuic(
dialCtx,
&quic.Config{},
tlsConfig,
edgeAddrPort,
nil,
math.MaxUint8,
logger,
dialopts.DialOpts{SkipPortReuse: true},
)
if err != nil {
return CheckResult{
Type: ProbeTypeQUIC,
Component: componentUDPConnectivity,
Target: targetPortQUIC,
ProbeStatus: Fail,
Details: detailsQUICHandshakeFailed,
Action: actionQUICBlocked,
}
}
if err := conn.CloseWithError(0, "precheck complete"); err != nil {
logger.Debug().Err(err).Msg("Failed to close QUIC connection after successful handshake")
}
return CheckResult{
Type: ProbeTypeQUIC,
Component: componentUDPConnectivity,
Target: targetPortQUIC,
ProbeStatus: Pass,
Details: detailsQUICHandshakeSuccessful,
}
}
// probeHTTP2 performs a TCP + TLS handshake to a single edge address and
// returns a CheckResult. The connection is closed immediately after the
// handshake no HTTP/2 frames are sent to keep the probe minimal. The probe
// SNI (probe.cftunnel.com) is used instead of the production h2.cftunnel.com
// to prevent OTD log noise.
//
// The dial timeout is capped at perProbeDialTimeout so that a single blocked
// dial cannot exhaust the entire suite budget.
func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, addr *allregions.EdgeAddr) CheckResult {
conn, err := dialer.DialEdge(ctx, perProbeDialTimeout, tlsConfig, addr.TCP, nil)
if err != nil {
return CheckResult{
Type: ProbeTypeHTTP2,
Component: componentTCPConnectivity,
Target: targetPortHTTP2,
ProbeStatus: Fail,
Details: detailsHTTP2BlockedOrUnreachable,
Action: actionHTTP2Blocked,
}
}
_ = conn.Close()
return CheckResult{
Type: ProbeTypeHTTP2,
Component: componentTCPConnectivity,
Target: targetPortHTTP2,
ProbeStatus: Pass,
Details: detailsHTTP2HandshakeSuccessful,
}
}
// probeManagementAPI tests TCP connectivity to api.cloudflare.com:443. A
// successful TCP connection (no TLS handshake required) confirms the port is
// reachable. This probe is always a soft failure: the tunnel can run without
// it, but automatic software updates will be unavailable.
func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResult {
dialCtx, cancel := context.WithTimeout(ctx, perProbeDialTimeout)
defer cancel()
conn, err := dialer.DialContext(dialCtx, "tcp", targetAPI)
if err != nil {
return CheckResult{
Type: ProbeTypeManagementAPI,
Component: componentCloudflareAPI,
Target: targetAPI,
ProbeStatus: Fail,
Details: detailsAPIConnectionFailed,
Action: actionAPIUnreachable,
}
}
_ = conn.Close()
return CheckResult{
Type: ProbeTypeManagementAPI,
Component: componentCloudflareAPI,
Target: targetAPI,
ProbeStatus: Pass,
Details: detailsApiReachable,
}
}
func skipResult(probeType ProbeType, component, target string, details string) CheckResult {
return CheckResult{
Type: probeType,
Component: component,
Target: target,
ProbeStatus: Skip,
Details: details,
}
}
// newDNSCheckResult creates a DNS CheckResult with the given fields.
// Type and Component are always ProbeTypeDNS and componentDNSResolution.
func newDNSCheckResult(target string, status Status, details, action string) CheckResult {
return CheckResult{
Type: ProbeTypeDNS,
Component: componentDNSResolution,
Target: target,
ProbeStatus: status,
Details: details,
Action: action,
}
}
// regionTargets returns the human-readable hostnames for region1 and region2
// based on the optional region flag value.
func regionTargets(region string) (string, string) {
switch region {
case "us":
return region1US, region2US
case "fed":
return region1Fed, region2Fed
default:
return region1Global, region2Global
}
}
// addrsByFamily extracts one V4 and one V6 address from a resolved CNAME group
// using allregions.NewRegion so that the IP-version preference logic matches
// production exactly. When cfg.IPVersion restricts to a single family the
// excluded family's pointer is nil.
func addrsByFamily(group []*allregions.EdgeAddr, ipVersion allregions.ConfigIPVersion) (v4, v6 *allregions.EdgeAddr) {
if ipVersion != allregions.IPv6Only {
v4 = allregions.NewRegion(group, allregions.IPv4Only).GetAnyAddress()
}
if ipVersion != allregions.IPv4Only {
v6 = allregions.NewRegion(group, allregions.IPv6Only).GetAnyAddress()
}
return
}
// runDNSProbe runs probeDNS with retry and returns []ResolvedTarget.
func runDNSProbe(ctx context.Context, resolver DNSResolver, region string) []ResolvedTarget {
var targets []ResolvedTarget
withRetry(ctx, maxRetries, func() bool {
targets = probeDNS(resolver, region)
for _, t := range targets {
if t.DNSResult.ProbeStatus == Fail {
return false
}
}
return len(targets) > 0
})
return targets
}
// resolveStaticEdge resolves each --edge addr individually, returning one
// ResolvedTarget per addr. Unresolvable addrs produce a Fail ResolvedTarget
// with nil Addrs so the report shows which addresses could not be reached.
func resolveStaticEdge(addrs []string, log *zerolog.Logger) []ResolvedTarget {
targets := make([]ResolvedTarget, 0, len(addrs))
for _, addr := range addrs {
resolved := allregions.ResolveAddrs([]string{addr}, log)
if len(resolved) > 0 {
targets = append(targets, ResolvedTarget{
Addrs: resolved,
DNSResult: newDNSCheckResult(addr, Pass, dnsResolvedSuccessfully, ""),
})
} else {
targets = append(targets, ResolvedTarget{
DNSResult: newDNSCheckResult(addr, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, addr, addr)),
})
}
}
return targets
}
+538
View File
@@ -0,0 +1,538 @@
package prechecks
import (
"context"
"crypto/tls"
"errors"
"net"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/mocks"
)
// Test constants for repeated string values.
const (
testRegion1Global = region1Global
testRegion2Global = region2Global
testRegion1US = region1US
testRegion2US = region2US
testRegion1Fed = region1Fed
testRegion2Fed = region2Fed
testEdgePort = 7844
)
// testTLSConfig is a minimal *tls.Config for tests. Mock dialers never
// perform a real TLS handshake, so an empty config is sufficient.
var testTLSConfig = &tls.Config{} //nolint:gosec
// Helper to create test edge addresses.
func createTestEdgeAddr(ip string, port int, version allregions.EdgeIPVersion) *allregions.EdgeAddr {
parsedIP := net.ParseIP(ip)
return &allregions.EdgeAddr{
TCP: &net.TCPAddr{IP: parsedIP, Port: port},
UDP: &net.UDPAddr{IP: parsedIP, Port: port},
IPVersion: version,
}
}
// probeDNS tests.
func TestProbeDNS_Success(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
v6Addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr, v6Addr}}, nil)
targets := probeDNS(resolver, "")
require.Len(t, targets, 1)
assert.NotEmpty(t, targets[0].Addrs)
assert.Equal(t, ProbeTypeDNS, targets[0].DNSResult.Type)
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsResolvedSuccessfully, targets[0].DNSResult.Details)
}
func TestProbeDNS_MultipleRegions(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
v4Addr1 := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
v4Addr2 := createTestEdgeAddr("192.0.2.2", testEdgePort, allregions.V4)
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{v4Addr1}, {v4Addr2}}, nil)
targets := probeDNS(resolver, "")
require.Len(t, targets, 2)
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[0].Addrs)
assert.Equal(t, testRegion2Global, targets[1].DNSResult.Target)
assert.Equal(t, Pass, targets[1].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[1].Addrs)
}
func TestProbeDNS_ResolverError(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return(nil, errors.New("DNS lookup failed"))
targets := probeDNS(resolver, "")
require.Len(t, targets, 2)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, "DNS lookup failed", targets[0].DNSResult.Details)
assert.Contains(t, targets[0].DNSResult.Action, testRegion1Global)
assert.Empty(t, targets[1].Addrs)
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
assert.Contains(t, targets[1].DNSResult.Action, testRegion2Global)
}
func TestProbeDNS_EmptyResults(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{}, nil)
targets := probeDNS(resolver, "")
require.Len(t, targets, 2)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
}
func TestProbeDNS_EmptyGroup(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("").Return([][]*allregions.EdgeAddr{{}}, nil)
targets := probeDNS(resolver, "")
require.Len(t, targets, 1)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
}
func TestProbeDNS_RegionFlag(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
resolver := mocks.NewMockDNSResolver(ctrl)
resolver.EXPECT().Resolve("us").Return([][]*allregions.EdgeAddr{{v4Addr}}, nil)
targets := probeDNS(resolver, "us")
require.Len(t, targets, 1)
assert.Equal(t, testRegion1US, targets[0].DNSResult.Target)
}
// probeQUIC tests.
func TestProbeQUIC_Success(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
successfulQUICConn := mocks.NewMockQUICConnection(ctrl)
successfulQUICConn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil)
dialer := mocks.NewMockQUICDialer(ctrl)
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(successfulQUICConn, nil)
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
logger := zerolog.New(nil)
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
func TestProbeQUIC_DialError(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dialer := mocks.NewMockQUICDialer(ctrl)
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("connection refused"))
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
logger := zerolog.New(nil)
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
assert.Equal(t, actionQUICBlocked, result.Action)
}
func TestProbeQUIC_CloseErrorDoesNotAffectResult(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Return a mock whose CloseWithError returns an error — probeQUIC must still
// report Pass because the handshake itself succeeded.
fakeQUICConn := mocks.NewMockQUICConnection(ctrl)
fakeQUICConn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(errors.New("close failed"))
dialer := mocks.NewMockQUICDialer(ctrl)
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fakeQUICConn, nil)
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
logger := zerolog.New(nil)
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
func TestProbeQUIC_ContextTimeout(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dialer := mocks.NewMockQUICDialer(ctrl)
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, context.DeadlineExceeded)
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
logger := zerolog.New(nil)
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
}
// probeHTTP2 tests.
func TestProbeHTTP2_Success(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dialer := mocks.NewMockTCPDialer(ctrl)
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&net.TCPConn{}, nil)
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
assert.Equal(t, ProbeTypeHTTP2, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsHTTP2HandshakeSuccessful, result.Details)
}
func TestProbeHTTP2_DialError(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dialer := mocks.NewMockTCPDialer(ctrl)
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("connection refused"))
addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
assert.Equal(t, ProbeTypeHTTP2, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsHTTP2BlockedOrUnreachable, result.Details)
assert.Equal(t, actionHTTP2Blocked, result.Action)
}
// probeManagementAPI tests.
func TestProbeManagementAPI_Success(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dialer := mocks.NewMockManagementDialer(ctrl)
dialer.EXPECT().DialContext(gomock.Any(), "tcp", "api.cloudflare.com:443").Return(&net.TCPConn{}, nil)
result := probeManagementAPI(context.Background(), dialer)
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
assert.Equal(t, "Cloudflare API", result.Component)
assert.Equal(t, "api.cloudflare.com:443", result.Target)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsApiReachable, result.Details)
}
func TestProbeManagementAPI_DialError(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dialer := mocks.NewMockManagementDialer(ctrl)
dialer.EXPECT().DialContext(gomock.Any(), "tcp", "api.cloudflare.com:443").Return(nil, errors.New("connection refused"))
result := probeManagementAPI(context.Background(), dialer)
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsAPIConnectionFailed, result.Details)
assert.Equal(t, actionAPIUnreachable, result.Action)
}
// skipResult tests.
func TestSkipResult(t *testing.T) {
t.Parallel()
result := skipResult(ProbeTypeQUIC, "UDP Connectivity", "Port 7844 (QUIC)", detailsDNSPrerequisiteFailed)
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, "UDP Connectivity", result.Component)
assert.Equal(t, "Port 7844 (QUIC)", result.Target)
assert.Equal(t, Skip, result.ProbeStatus)
assert.Equal(t, detailsDNSPrerequisiteFailed, result.Details)
}
// regionTargets tests.
func TestRegionTargets(t *testing.T) {
t.Parallel()
tests := []struct {
name string
region string
wantRegion1 string
wantRegion2 string
description string
}{
{
name: "empty region returns global hostnames",
region: "",
wantRegion1: testRegion1Global,
wantRegion2: testRegion2Global,
},
{
name: "us region returns US hostnames",
region: "us",
wantRegion1: testRegion1US,
wantRegion2: testRegion2US,
},
{
name: "fed region returns fed hostnames",
region: "fed",
wantRegion1: testRegion1Fed,
wantRegion2: testRegion2Fed,
},
{
name: "unknown region defaults to global hostnames",
region: "eu",
wantRegion1: testRegion1Global,
wantRegion2: testRegion2Global,
description: "Unknown regions should default to global hostnames",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotR1, gotR2 := regionTargets(tt.region)
assert.Equal(t, tt.wantRegion1, gotR1)
assert.Equal(t, tt.wantRegion2, gotR2)
})
}
}
// addrsByFamily tests.
func TestAddrsByFamily(t *testing.T) {
t.Parallel()
v4Addr := createTestEdgeAddr("192.0.2.1", testEdgePort, allregions.V4)
v6Addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
tests := []struct {
name string
group []*allregions.EdgeAddr
ipVersion allregions.ConfigIPVersion
wantV4 bool
wantV6 bool
}{
{
name: "auto returns both v4 and v6",
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
ipVersion: allregions.Auto,
wantV4: true,
wantV6: true,
},
{
name: "ipv4 only returns v4 and nil v6",
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
ipVersion: allregions.IPv4Only,
wantV4: true,
wantV6: false,
},
{
name: "ipv6 only returns nil v4 and v6",
group: []*allregions.EdgeAddr{v4Addr, v6Addr},
ipVersion: allregions.IPv6Only,
wantV4: false,
wantV6: true,
},
{
name: "empty group returns nil for both",
group: []*allregions.EdgeAddr{},
ipVersion: allregions.Auto,
wantV4: false,
wantV6: false,
},
{
name: "only v4 available returns v4 and nil v6",
group: []*allregions.EdgeAddr{v4Addr},
ipVersion: allregions.Auto,
wantV4: true,
wantV6: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotV4, gotV6 := addrsByFamily(tt.group, tt.ipVersion)
if tt.wantV4 {
assert.NotNil(t, gotV4)
} else {
assert.Nil(t, gotV4)
}
if tt.wantV6 {
assert.NotNil(t, gotV6)
} else {
assert.Nil(t, gotV6)
}
})
}
}
// IPv6 address tests for probeQUIC.
func TestProbeQUIC_IPv6Address(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
successfulQUICConn := mocks.NewMockQUICConnection(ctrl)
successfulQUICConn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil)
dialer := mocks.NewMockQUICDialer(ctrl)
dialer.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(successfulQUICConn, nil)
addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
logger := zerolog.New(nil)
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
// IPv6 address tests for probeHTTP2.
func TestProbeHTTP2_IPv6Address(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dialer := mocks.NewMockTCPDialer(ctrl)
dialer.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&net.TCPConn{}, nil)
addr := createTestEdgeAddr("2001:db8::1", testEdgePort, allregions.V6)
result := probeHTTP2(context.Background(), testTLSConfig, dialer, addr)
assert.Equal(t, Pass, result.ProbeStatus)
}
// resolveStaticEdge tests.
// TestResolveStaticEdge_SingleAddr verifies that a single resolvable --edge
// addr produces one group labeled with the original addr string.
func TestResolveStaticEdge_SingleAddr(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
targets := resolveStaticEdge([]string{"127.0.0.1:7844"}, &logger)
require.Len(t, targets, 1)
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[0].Addrs)
}
// TestResolveStaticEdge_MultipleAddrs verifies that multiple --edge addrs each
// produce their own ResolvedTarget, preserving per-addr structure and label order.
func TestResolveStaticEdge_MultipleAddrs(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "127.0.0.2:7844"}, &logger)
require.Len(t, targets, 2)
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
assert.Equal(t, "127.0.0.2:7844", targets[1].DNSResult.Target)
}
// TestResolveStaticEdge_InvalidAddr verifies that an unresolvable addr is
// silently skipped and does not appear in the output.
func TestResolveStaticEdge_InvalidAddr(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
// "not-a-valid-addr" has no port — ResolveTCPAddr will fail.
targets := resolveStaticEdge([]string{"not-a-valid-addr"}, &logger)
require.Len(t, targets, 1)
assert.Equal(t, "not-a-valid-addr", targets[0].DNSResult.Target)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
assert.Empty(t, targets[0].Addrs)
}
// TestResolveStaticEdge_PartiallyValid verifies that a mix of valid and invalid
// addrs produces one ResolvedTarget per addr — valid ones with Addrs and a Skip
// DNSResult, invalid ones with nil Addrs and a Fail DNSResult.
func TestResolveStaticEdge_PartiallyValid(t *testing.T) {
t.Parallel()
logger := zerolog.Nop()
targets := resolveStaticEdge([]string{"127.0.0.1:7844", "not-a-valid-addr", "127.0.0.2:7844"}, &logger)
require.Len(t, targets, 3)
assert.Equal(t, "127.0.0.1:7844", targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[0].Addrs)
assert.Equal(t, "not-a-valid-addr", targets[1].DNSResult.Target)
assert.Equal(t, Fail, targets[1].DNSResult.ProbeStatus)
assert.Empty(t, targets[1].Addrs)
assert.Equal(t, "127.0.0.2:7844", targets[2].DNSResult.Target)
assert.Equal(t, Pass, targets[2].DNSResult.ProbeStatus)
assert.NotEmpty(t, targets[2].Addrs)
}

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