Compare commits

...

292 Commits

Author SHA1 Message Date
Devin Carr ce27840573 TUN-9291: Remove dynamic reloading of features for datagram v3
During a refresh of the supported features via the DNS TXT record,
cloudflared would update the internal feature list, but would not
propagate this information to the edge during a new connection.

This meant that a situation could occur in which cloudflared would
think that the client's connection could support datagram V3, and
would setup that muxer locally, but would not propagate that information
to the edge during a register connection in the `ClientInfo` of the
`ConnectionOptions`. This meant that the edge still thought that the
client was setup to support datagram V2 and since the protocols are
not backwards compatible, the local muxer for datagram V3 would reject
the incoming RPC calls.

To address this, the feature list will be fetched only once during
client bootstrapping and will persist as-is until the client is restarted.
This helps reduce the complexity involved with different connections
having possibly different sets of features when connecting to the edge.
The features will now be tied to the client and never diverge across
connections.

Also, retires the use of `support_datagram_v3` in-favor of
`support_datagram_v3_1` to reduce the risk of reusing the feature key.
The `dv3` TXT feature key is also deprecated.

Closes TUN-9291
2025-05-07 23:21:08 +00:00
GoncaloGarcia 40dc601e9d Release 2025.4.2 2025-04-30 14:15:20 +01:00
João "Pisco" Fernandes e5578cb74e Release 2025.4.1 2025-04-30 13:10:45 +01:00
João "Pisco" Fernandes bb765e741d chore: Do not use gitlab merge request pipelines
## Summary
If we define pipelines to trigger on merge requests,
they will take precedence over branch pipelines,
which is currently the way our old pipelines are still
triggered. This means that we can have a merge request
with green pipelines, but actually the external pipelines failed.
Therefore, we need to only rely on branch pipelines,
to ensure that we don't ignore the results from
external pipelines.

More information here:
- https://forum.gitlab.com/t/merge-request-considering-merge-request-pipelines-instead-of-branch-pipelines/111248/2
- https://docs.gitlab.com/17.6/ci/jobs/job_rules/#run-jobs-only-in-specific-pipeline-types
2025-04-30 12:01:43 +00:00
João "Pisco" Fernandes 10081602a4 Release 2025.4.1 2025-04-30 11:09:14 +01:00
Gonçalo Garcia 236fcf56d6 DEVTOOLS-16383: Create GitlabCI pipeline to release Mac builds
Adds a new Gitlab CI pipeline that releases cloudflared Mac builds and replaces the Teamcity adhoc job.
This will build, sign and create a new Github release or add the artifacts to an existing release if the other jobs finish first.
2025-04-30 09:57:52 +00:00
João "Pisco" Fernandes 73a9980f38 TUN-9255: Improve flush on write conditions in http2 tunnel type to match what is done on the edge
## Summary
We have adapted our edge services to better know when they should flush on write. This is an important
feature to ensure response types like Server Side Events are not buffered, and instead are propagated to the eyeball
as soon as possible. This commit implements a similar logic for http2 tunnel protocol that we use in our edge
services. By adding the new events stream header for json `application/x-ndjson` and using the content-length
and transfer-encoding headers as well, following the RFC's:
- https://datatracker.ietf.org/doc/html/rfc7230#section-4.1
- https://datatracker.ietf.org/doc/html/rfc9112#section-6.1

Closes TUN-9255
2025-04-24 11:49:19 +00:00
Tom Lianza 86e8585563 SDLC-3727 - Adding FIPS status to backstage
## Summary
This is a documentation change to help make sure we have an accurate FIPS inventory: https://wiki.cfdata.org/display/ENG/RFC%3A+Scalable+approach+for+managing+FIPS+compliance

Closes SDLC-3727
2025-04-10 16:58:04 +00:00
João "Pisco" Fernandes d8a066628b Release 2025.4.0 2025-04-01 20:23:54 +01:00
João "Pisco" Fernandes 553e77e061 chore: fix linter rules 2025-04-01 18:57:55 +01:00
Cyb3r Jak3 8f94f54ec7
feat: Adds a new command line for tunnel run for token file
Adds a new command line flag for `tunnel run` which allows a file to be
read for the token. I've left the token command line argument with
priority.
2025-04-01 18:23:22 +01:00
gofastasf 2827b2fe8f
fix: Use path and filepath operation appropriately
Using path package methods can cause errors on windows machines.

path methods are used for url operations and unix specific operation.

filepath methods are used for file system paths and its cross platform. 

Remove strings.HasSuffix and use filepath.Ext and path.Ext for file and
url extenstions respectively.
2025-04-01 17:59:43 +01:00
Rohan Mukherjee 6dc8ed710e
fix: expand home directory for credentials file
## Issue

The [documentation for creating a tunnel's configuration
file](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-local-tunnel/#4-create-a-configuration-file)
does not specify that the `credentials-file` field in `config.yml` needs
to be an absolute path.

A user (E.G. me 🤦) might add a path like `~/.cloudflared/<uuid>.json`
and wonder why the `cloudflared tunnel run` command is throwing a
credentials file not found error. Although one might consider it
intuitive, it's not a fair assumption as a lot of CLI tools allow file
paths with `~` for specifying files.

P.S. The tunnel ID in the following snippet is not a real tunnel ID, I
just generated it.
```
url: http://localhost:8000
tunnel: 958a1ef6-ff8c-4455-825a-5aed91242135
credentials-file: ~/.cloudflared/958a1ef6-ff8c-4455-825a-5aed91242135.json
```

Furthermore, the error has a confusing message for the user as the file
at the logged path actually exists, it is just that `os.Stat` failed
because it could not expand the `~`.

## Solution

This commit fixes the above issue by running a `homedir.Expand` on the
`credentials-file` path in the `credentialFinder` function.
2025-04-01 17:54:57 +01:00
Shereef Marzouk e0b1ac0d05
chore: Update tunnel configuration link in the readme 2025-04-01 17:53:29 +01:00
Bernhard M. Wiedemann e7c5eb54af
Use RELEASE_NOTES date instead of build date
Use `RELEASE_NOTES` date instead of build date
to make builds reproducible.
See https://reproducible-builds.org/ for why this is good
and https://reproducible-builds.org/specs/source-date-epoch/
for the definition of this variable.
This date call only works with GNU date and BSD date.

Alternatively,
https://reproducible-builds.org/docs/source-date-epoch/#makefile could
be implemented.

This patch was done while working on reproducible builds for openSUSE,
sponsored by the NLnet NGI0 fund.
2025-04-01 17:52:50 +01:00
teslaedison cfec602fa7
chore: remove repetitive words 2025-04-01 17:51:57 +01:00
Micah Yeager 6fceb94998
feat: emit explicit errors for the `service` command on unsupported OSes
Per the contribution guidelines, this seemed to me like a small enough
change to not warrant an issue before creating this pull request. Let me
know if you'd like me to create one anyway.

## Background

While working with `cloudflared` on FreeBSD recently, I noticed that
there's an inconsistency with the available CLI commands on that OS
versus others — namely that the `service` command doesn't exist at all
for operating systems other than Linux, macOS, and Windows.

Contrast `cloudflared --help` output on macOS versus FreeBSD (truncated
to focus on the `COMMANDS` section):

- Current help output on macOS:

  ```text
  COMMANDS:
     update     Update the agent if a new version exists
     version    Print the version
     proxy-dns  Run a DNS over HTTPS proxy server.
     tail       Stream logs from a remote cloudflared
     service    Manages the cloudflared launch agent
     help, h    Shows a list of commands or help for one command
     Access:
       access, forward  access <subcommand>
     Tunnel:
tunnel Use Cloudflare Tunnel to expose private services to the Internet
or to Cloudflare connected private users.
  ```
- Current help output on FreeBSD:
  ```text
  COMMANDS:
     update     Update the agent if a new version exists
     version    Print the version
     proxy-dns  Run a DNS over HTTPS proxy server.
     tail       Stream logs from a remote cloudflared
     help, h    Shows a list of commands or help for one command
     Access:
       access, forward  access <subcommand>
     Tunnel:
tunnel Use Cloudflare Tunnel to expose private services to the Internet
or to Cloudflare connected private users.
  ```

This omission has caused confusion for users (including me), especially
since the provided command in the Cloudflare Zero Trust dashboard
returns a seemingly-unrelated error message:

```console
$ sudo cloudflared service install ...
You did not specify any valid additional argument to the cloudflared tunnel command.

If you are trying to run a Quick Tunnel then you need to explicitly pass the --url flag.
Eg. cloudflared tunnel --url localhost:8080/.

Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes.
For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)
```

## Contribution

This pull request adds a "stub" `service` command (including the usual
subcommands available on other OSes) to explicitly declare it as
unsupported on the operating system.

New help output on FreeBSD (and other operating systems where service
management is unsupported):

```text
COMMANDS:
   update     Update the agent if a new version exists
   version    Print the version
   proxy-dns  Run a DNS over HTTPS proxy server.
   tail       Stream logs from a remote cloudflared
   service    Manages the cloudflared system service (not supported on this operating system)
   help, h    Shows a list of commands or help for one command
   Access:
     access, forward  access <subcommand>
   Tunnel:
     tunnel  Use Cloudflare Tunnel to expose private services to the Internet or to   Cloudflare connected private users.
```

New outputs when running the service management subcommands:

```console
$ sudo cloudflared service install ...
service installation is not supported on this operating system
```

```console
$ sudo cloudflared service uninstall ...
service uninstallation is not supported on this operating system
```

This keeps the available commands consistent until proper service
management support can be added for these otherwise-supported operating
systems.
2025-04-01 17:48:20 +01:00
Roman cf817f7036
Fix messages to point to one.dash.cloudflare.com 2025-04-01 17:47:23 +01:00
VFLC c8724a290a
Fix broken links in `cmd/cloudflared/*.go` related to running tunnel as a service
This PR updates 3 broken links to document [run tunnel as a
service](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/local-management/as-a-service/).
2025-04-01 17:45:59 +01:00
João "Pisco" Fernandes e7586153be TUN-9101: Don't ignore errors on `cloudflared access ssh`
## Summary

This change ensures that errors resulting from the `cloudflared access ssh` call are no longer ignored. By returning the error from `carrier.StartClient` to the upstream, we ensure that these errors are properly logged on stdout, providing better visibility and debugging capabilities.

Relates to TUN-9101
2025-03-17 18:42:19 +00:00
Chung-Ting Huang 11777db304 TUN-9089: Pin go import to v0.30.0, v0.31.0 requires go 1.23
Closes TUN-9089
2025-03-06 12:05:24 +00:00
lneto 3f6b1f24d0 Release 2025.2.1 2025-02-26 16:44:32 +00:00
Luis Neto a4105e8708 TUN-9016: update base-debian to v12
## Summary

Fixes vulnerability ([CVE -2024-4741](https://github.com/advisories/GHSA-6vgq-8qjq-h578))

 Closes TUN-9016
2025-02-26 15:54:10 +00:00
Luis Neto 6496322bee TUN-9007: modify logic to resolve region when the tunnel token has an endpoint field
## Summary

Within the work of FEDRamp it is necessary to change the HA SD lookup to use as srv `fed-v2-origintunneld`

This work assumes that the tunnel token has an optional endpoint field which will be used to modify the behaviour of the HA SD lookup.

Finally, the presence of the endpoint will override region to _fed_ and fail if any value is passed for the flag region.

Closes TUN-9007
2025-02-25 19:03:41 +00:00
Luis Neto 906452a9c9 TUN-8960: Connect to FED API GW based on the OriginCert's endpoint
## Summary

Within the scope of the FEDRamp High RM, it is necessary to detect if an user should connect to a FEDRamp colo.

At first, it was considered to add the --fedramp as global flag however this could be a footgun for the user or even an hindrance, thus, the proposal is to save in the token (during login) if the user authenticated using the FEDRamp Dashboard. This solution makes it easier to the user as they will only be required to pass the flag in login and nothing else.

* Introduces the new field, endpoint, in OriginCert
* Refactors login to remove the private key and certificate which are no longer used
* Login will only store the Argo Tunnel Token
* Remove namedTunnelToken as it was only used to for serialization

Closes TUN-8960
2025-02-25 17:13:33 +00:00
Jingqi Huang d969fdec3e SDLC-3762: Remove backstage.io/source-location from catalog-info.yaml 2025-02-13 13:02:50 -08:00
João "Pisco" Fernandes 7336a1a4d6 TUN-8914: Create a flags module to group all cloudflared cli flags
## Summary

This commit refactors some of the flags of cloudflared to their own module, so that they can be used across the code without requiring to literal strings which are much more error prone.

 Closes TUN-8914
2025-02-06 03:30:27 -08:00
João "Pisco" Fernandes df5dafa6d7 Release 2025.2.0 2025-02-03 18:39:00 +00:00
Bas Westerbaan c19f919428 Bump x/crypto to 0.31.0 2025-02-03 16:08:02 +01:00
João "Pisco" Fernandes b187879e69 TUN-8914: Add a new configuration to locally override the max-active-flows
## Summary

This commit introduces a new command line flag, `--max-active-flows`, which allows overriding the remote configuration for the maximum number of active flows.

The flag can be used with the `run` command, like `cloudflared tunnel --no-autoupdate run --token <TUNNEL_TOKEN> --max-active-flows 50000`, or set via an environment variable `TUNNEL_MAX_ACTIVE_FLOWS`.

Note that locally-set values always take precedence over remote settings, even if the tunnel is remotely managed.

Closes TUN-8914
2025-02-03 03:42:50 -08:00
lneto 2feccd772c Release 2025.1.1 2025-01-30 14:48:47 +00:00
Luis Neto 90176a79b4 TUN-8894: report FIPS+PQ error to Sentry when dialling to the edge
## Summary

Since we will enable PQ + FIPS it is necessary to add observability so that we can understand if issues are happening.

 Closes TUN-8894
2025-01-30 06:26:53 -08:00
Luis Neto 9695829e5b TUN-8857: remove restriction for using FIPS and PQ
## Summary

When the FIPS compliance was achieved with HTTP/2 Transport the technology at the time wasn't available or certified to be used in tandem with Post-Quantum encryption. Nowadays, that is possible, thus, we can also remove this restriction from Cloudflared.

 Closes TUN-8857
2025-01-30 05:47:07 -08:00
Luis Neto 31a870b291 TUN-8855: Update PQ curve preferences
## Summary

Nowadays, Cloudflared only supports X25519Kyber768Draft00 (0x6399,25497) but older versions may use different preferences.

For FIPS compliance we are required to use P256Kyber768Draft00 (0xfe32,65074) which is supported in our internal fork of [Go-Boring-1.22.10](https://bitbucket.cfdata.org/projects/PLAT/repos/goboring/browse?at=refs/heads/go-boring/1.22.10 "Follow link").

In the near future, Go will support by default the X25519MLKEM768 (0x11ec,4588) given this we may drop the usage of our public fork of GO.

To summarise:

* Cloudflared FIPS: QUIC_CURVE_PREFERENCES=65074
* Cloudflared non-FIPS: QUIC_CURVE_PREFERENCES=4588

Closes TUN-8855
2025-01-30 05:02:47 -08:00
Luis Neto bfdb0c76dc TUN-8855: fix lint issues
## Summary

Fix lint issues necessary for a subsequent PR. This is only separate to allow a better code review of the actual changes.

Closes TUN-8855
2025-01-30 03:53:24 -08:00
Luis Neto 45f67c23fd TUN-8858: update go to 1.22.10 and include quic-go FIPS changes
## Summary

To have support for new curves and to achieve FIPS compliance Cloudflared must be released with [Go-Boring-1.22.10](https://bitbucket.cfdata.org/projects/PLAT/repos/goboring/browse?at=refs/heads/go-boring/1.22.10 "Follow link") along with the quic-go patches. 

 Closes TUN-8858
2025-01-30 03:11:54 -08:00
João "Pisco" Fernandes 0f1bfe99ce TUN-8904: Rename Connect Response Flow Rate Limited metadata
## Summary

This commit renames the public variable that identifies the metadata key and value for the ConnectResponse structure when the flow was rate limited.

 Closes TUN-8904
2025-01-22 07:23:46 -08:00
Eduardo Gomes 18eecaf151 AUTH-6633 Fix cloudflared access login + warp as auth
## Summary
cloudflared access login and cloudflared access curl fails when the Access application has warp_as_auth enabled.

This bug originates from a 4 year old inconsistency where tokens signed by the nginx-fl-access module include 'aud' as a string, while tokens signed by the access authentication worker include 'aud' as an array of strings.
When the new(ish) feature warp_as_auth is enabled for the app, the fl module signs the token as opposed to the worker like usually.


I'm going to bring this up to the Access team, and try to figure out a way to consolidate this discrepancy without breaking behaviour.

Meanwhile we have this [CUSTESC ](https://jira.cfdata.org/browse/CUSTESC-47987), so I'm making cloudflared more lenient by accepting both []string and string in the token 'aud' field.



Tested this by compiling and running cloudflared access curls to my domains


Closes AUTH-6633
2025-01-21 04:00:28 -08:00
João "Pisco" Fernandes 4eb0f8ce5f TUN-8861: Rename Session Limiter to Flow Limiter
## Summary
Session is the concept used for UDP flows. Therefore, to make
the session limiter ambiguous for both TCP and UDP, this commit
renames it to flow limiter.

Closes TUN-8861
2025-01-20 06:33:40 -08:00
João "Pisco" Fernandes 8c2eda16c1 TUN-8861: Add configuration for active sessions limiter
## Summary
This commit adds a new configuration in the warp routing
config to allow users to define the active sessions limit
value.
2025-01-20 11:39:42 +00:00
João "Pisco" Fernandes 8bfe111cab TUN-8861: Add session limiter to TCP session manager
## Summary
In order to make cloudflared behavior more predictable and
prevent an exhaustion of resources, we have decided to add
session limits that can be configured by the user. This commit
adds the session limiter to the HTTP/TCP handling path.
For now the limiter is set to run only in unlimited mode.
2025-01-20 10:53:53 +00:00
João "Pisco" Fernandes bf4954e96a TUN-8861: Add session limiter to UDP session manager
## Summary
In order to make cloudflared behavior more predictable and
prevent an exhaustion of resources, we have decided to add
session limits that can be configured by the user. This first
commit introduces the session limiter and adds it to the UDP
handling path. For now the limiter is set to run only in
unlimited mode.
2025-01-20 02:52:32 -08:00
Gonçalo Garcia 8918b6729e TUN-8871: Accept login flag to authenticate with Fedramp environment
## Summary
Some description...

Closes TUN-8871
2025-01-17 08:16:36 -08:00
João "Pisco" Fernandes 25c3f676f4 TUN-8900: Add import of Apple Developer Certificate Authority to macOS Pipeline
## Summary
During the renewal of the certificates used to sign the macOS binaries and package,
we faced an issue with the new certificates requiring a specific certification authority
that wasn't available in the keychain of the mac agents. Therefore, this commit adds
an import step that will ensure that the Certificate Authority, usually fetched from
https://www.apple.com/certificateauthority/ is imported into the keychain to validate
the Developer Certificates.

Closes TUN-8900
2025-01-17 07:10:16 -08:00
João "Pisco" Fernandes a1963aed80 TUN-8866: Add linter to cloudflared repository
## Summary
To improve our code, this commit adds a linter that will start
checking for issues from this commit onwards, also forcing
issues to be fixed on the file changed and not only on the changes
themselves. This should help improve our code quality overtime.

Closes TUN-8866
2025-01-16 07:02:54 -08:00
chungthuang ac34f94d42 TUN-8848: Don't treat connection shutdown as an error condition when RPC server is done 2025-01-09 10:07:12 -06:00
João "Pisco" Fernandes d8c7f1c1ec Release 2025.1.0 2025-01-07 11:33:38 +00:00
Devin Carr 3b522a27cf TUN-8807: Add support_datagram_v3 to remote feature rollout
Support rolling out the `support_datagram_v3` feature via remote feature rollout (DNS TXT record) with `dv3` key.

Consolidated some of the feature evaluation code into the features module to simplify the lookup of available features at runtime.

Reduced complexity for management logs feature lookup since it's a default feature.

Closes TUN-8807
2025-01-06 09:15:18 -08:00
João "Pisco" Fernandes 5cfe9bef79 TUN-8842: Add Ubuntu Noble and 'any' debian distributions to release script
## Summary
Ubuntu has released a new LTS version, and there are people starting to use it, this makes
our installation recommendation, that automatically detecs the release flavor, to fail for
Noble users. Therefore, this commit adds this new version to our release packages.
It also adds an `any` package so that we can update our documentation to use it since
we are using the same binaries across all debian flavors, so there is no reason to keep
adding more release flavors when we can just take advantage of the `any` release flavor
like other repositories do.
2025-01-06 12:09:13 +00:00
Luis Neto 2714d10d62 TUN-8829: add CONTAINER_BUILD to dockerfiles
Closes TUN-8829
2024-12-20 08:24:12 -08:00
lneto ac57ed9709 Release 2024.12.2 2024-12-19 15:28:18 +00:00
Gonçalo Garcia c6901551e7 TUN-8822: Prevent concurrent usage of ICMPDecoder
## Summary
Some description...

Closes TUN-8822
2024-12-19 07:19:36 -08:00
Luis Neto 9bc6cbd06d TUN-8818: update changes document to reflect newly added diag subcommand
Closes TUN-8818
2024-12-18 04:26:38 -08:00
Devin Carr bc9c5d2e6e TUN-8817: Increase close session channel by one since there are two writers
When closing a session, there are two possible signals that will occur,
one from the outside, indicating that the session is idle and needs to
be closed, and the internal error condition that will be unblocked
with a net.ErrClosed when the connection underneath is closed. Both of
these routines write to the session's closeChan.

Once the reader for the closeChan reads one value, it will immediately
return. This means that the channel is a one-shot and one of the two
writers will get stuck unless the size of the channel is increased to
accomodate for the second write to the channel.

With the channel size increased to two, the second writer (whichever
loses the race to write) will now be unblocked to end their go routine
and return.

Closes TUN-8817
2024-12-17 14:55:09 -08:00
Luis Neto 1859d742a8 TUN-8724: Add CLI command for diagnostic procedure
## Summary
Adds a new CLI subcommand, under the tunnel command, the `diag`. This command has as function the automatic collection of different data points, such as, logs, metrics, network information, system information, tunnel state, and runtime information which will be written to a single zip file.

Closes TUN-8724
2024-12-13 10:07:56 -08:00
Luis Neto 8ed19222b9 TUN-8797: update CHANGES.md with note about semi-deterministic approach used to bind metrics server
Closes TUN-8797
2024-12-13 01:53:11 -08:00
Luis Neto 02e7ffd5b7 TUN-8792: Make diag/system endpoint always return a JSON
## Summary
Change the system information collector and respective http handler so that it always returns a JSON.

Closes [TUN-8792](https://jira.cfdata.org/browse/TUN-8792)
2024-12-11 02:48:41 -08:00
Luis Neto ba9f28ef43 TUN-8786: calculate cli flags once for the diagnostic procedure
## Summary

The flags were always being computed when their value is static.

 Closes TUN-8786
2024-12-11 01:29:20 -08:00
Luis Neto 77b99cf5fe TUN-8784: Set JSON encoder options to print formatted JSON when writing diag files
## Summary

The initial implementation produced correct JSON however it was not formatted which would make it harder to read the file by an user.

 Closes TUN-8784
2024-12-10 13:01:24 -08:00
Luis Neto d74ca97b51 TUN-8785: include the icmp sources in the diag's tunnel state
Closes TUN-8785
2024-12-10 10:42:33 -08:00
Luis Neto 29f0cf354c TUN-8783: fix log collectors for the diagnostic procedure
## Summary

* The host log collector now verifies if the OS is linux and has systemd if so it will use journalctl to get the logs
* In linux systems docker will write the output of the command logs to the stderr therefore the function that handles the execution of the process will copy both the contents of stdout and stderr; this also affect the k8s collector

Closes TUN-8783
2024-12-10 09:53:13 -08:00
lneto e7dcb6edca Release 2024.12.1 2024-12-10 16:07:33 +00:00
Luis Neto 14cf0eff1d TUN-8795: update createrepo to createrepo_c to fix the release_pkgs.py script
## Summary
The default-flavour of cfsetup changed from bullseye to bookworm and in the latter the createrepo package was renamed to createrepo_c.

Closes TUN-8795
2024-12-10 08:07:02 -08:00
lneto a00c80f9e1 Release 2024.12.0 2024-12-09 16:55:31 +00:00
Luis Neto 12d878531c TUN-8789: make python package installation consistent
## Summary

The previous changes regarding python's distribution which broke CI the installation of python packages.

Python packages in cfsetup are now installed via virtual environment. The dependency python3-venv was added as builddep to allow the creation of venv and the python packages installation was moved to the post-cache resulting in the removal of 

* anchor build_release_pre_cache
* anchor component_test_pre_cache

Closes TUN-8789
2024-12-09 08:52:48 -08:00
Devin Carr 588ab7ebaa TUN-8640: Add ICMP support for datagram V3
Closes TUN-8640
2024-12-09 07:23:11 -08:00
Joe Groocock dfbccd917c TUN-8781: Add Trixie, drop Buster. Default to Bookworm
Closes TUN-8781
2024-12-06 05:04:16 -08:00
Devin Carr 37010529bc TUN-8775: Make sure the session Close can only be called once
The previous capture of the sync.OnceValue was re-initialized for each
call to `Close`. This needed to be initialized during the creation of
the session to ensure that the sync.OnceValue reference was held for
the session's lifetime.

Closes TUN-8775
2024-12-05 14:12:53 -08:00
Luis Neto f07d04d129 TUN-8768: add job report to diagnostic zipfile
## Summary

Add a new job that write to a file the result of all of the other tasks along with possible errors. This file is also added to the root of the diagnostic zip file.

 Closes TUN-8768
2024-12-04 10:32:49 -08:00
Luis Neto f12036c2da TUN-8770: add cli configuration and tunnel configuration to diagnostic zipfile
## Summary

Adds two new jobs which will export the cli configuration and tunnel configuration in separate files. These files will also be added to the zipfile's root.

 Closes TUN-8770
2024-12-04 05:26:51 -08:00
Luis Neto 520e266411 TUN-8767: include raw output from network collector in diagnostic zipfile
## Summary

Export raw format of traceroute is widely known and useful for debugging. This raw output is written to the zipfile's root at the end of the diagnostic.

Closes TUN-8767
2024-12-04 04:40:51 -08:00
Luis Neto 7bd86762a7 TUN-8725: implement diagnostic procedure
## Summary
Add a function that orchestrates the diagnostic jobs producing a zip file at the end.

Closes TUN-8725
2024-12-04 03:37:57 -08:00
lneto 451f98e1d1 TUN-8727: extend client to include function to get cli configuration and tunnel configuration 2024-12-03 21:39:03 +00:00
Luis Neto 60fe4a0800 TUN-8769: fix k8s log collector arguments
## Summary

The equal signs were making the exec command to fail removing them fixes the issue.

Closes TUN-8769
2024-12-03 06:24:14 -08:00
Luis Neto 1ef109c042 TUN-8762: fix argument order when invoking tracert and modify network info output parsing.
## Summary

The windows code path has three bugs:

* the -4 and -6 option cannot be passed in the last position
* since there are some lines in the command output that are not parsable the collection fails to parse any kind of output
* the timeout hop is not correctly parsed

This PR also guards the parsing code against empty domains

Closes TUN-8762
2024-12-03 04:56:28 -08:00
Luis Neto 65786597cc TUN-8732: implement port selection algorithm
## Summary
Implements the discovery of the metrics server given an array of addresses (known addresses), tunnelID, and connectorID.

Closes TUN-8732
2024-12-03 04:07:55 -08:00
Luis Neto f884b29d0d TUN-8726: implement compression routine to be used in diagnostic procedure
## Summary
Basic routine to compress the diagnostic files into the root of a zipfile.

Closes TUN-8726
2024-12-03 03:27:04 -08:00
Luis Neto b3304bf05b TUN-8727: implement metrics, runtime, system, and tunnelstate in diagnostic http client
## Summary
The diagnostic procedure needs to extract information available in the metrics server via HTTP calls. 
These changes add to the diagnostic client the remaining endpoints.

Closes TUN-8727
2024-11-29 09:08:42 -08:00
Luis Neto 28796c659e TUN-8729: implement network collection for diagnostic procedure
## Summary
This PR adds implementation for windows & unix that collect the tracert.exe & traceroute output in the form of hops.

Closes TUN-8729
2024-11-29 07:43:36 -08:00
Devin Carr 9da15b5d96 TUN-8640: Refactor ICMPRouter to support new ICMPResponders
A new ICMPResponder interface is introduced to provide different
implementations of how the ICMP flows should return to the QUIC
connection muxer.

Improves usages of netip.AddrPort to leverage the embedded zone
field for IPv6 addresses.

Closes TUN-8640
2024-11-27 12:46:08 -08:00
Luis Neto 46dc6316f9 TUN-8734: add log collection for kubernetes
## Summary
Adds the log collector for K8s based deployments.

Closes TUN-8734
2024-11-27 03:15:15 -08:00
Luis Neto 16e65c70ad TUN-8733: add log collection for docker
## Summary
Adds the log collector for docker based deployments

Closes TUN-8733
2024-11-27 01:08:54 -08:00
Luis Neto a6f9e68739 TUN-8735: add managed/local log collection
## Summary
Adds a log collector for the managed/local runtimes.


Closes TUN-8735 TUN-8736
2024-11-26 10:30:44 -08:00
Luis Neto f85c0f1cc0 TUN-8730: implement diag/configuration
Implements the endpoint that retrieves the configuration of a running instance.

The configuration consists in a map of cli flag to the provided value along with the uid that of the user that started the process
2024-11-25 11:24:51 -08:00
Luis Neto 4b0b6dc8c6 TUN-8728: implement diag/tunnel endpoint
## Summary
The new endpoint returns the current information to be used when calling the diagnostic procedure.
This also adds:
- add indexed connection info and method to extract active connections from connTracker
- add edge address to Event struct and conn tracker
- remove unnecessary event send
- add tunnel configuration handler
- adjust cmd and metrics to create diagnostic server

Closes TUN-8728
2024-11-25 10:43:32 -08:00
Luis Neto aab5364252 TUN-8731: Implement diag/system endpoint
## Summary
This PR will add a new endpoint, "diag/system" to the metrics server that collects system information from different operating systems.

Closes TUN-8731
2024-11-22 08:10:05 -08:00
Luis Neto e2c2b012f1 TUN-8737: update metrics server port selection
## Summary
Update how metrics server binds to a listener by using a known set of ports whenever the default address is used with the fallback to a random port in case all address are already in use. The default address changes at compile time in order to bind to a different default address when the final deliverable is a docker image.

Refactor ReadyServer tests.

Closes TUN-8737
2024-11-22 07:23:46 -08:00
Devin Carr d779394748 TUN-8748: Migrated datagram V3 flows to use migrated context
Previously, during local flow migration the current connection context
was not part of the migration and would cause the flow to still be listening
on the connection context of the old connection (before the migration).
This meant that if a flow was migrated from connection 0 to
connection 1, and connection 0 goes away, the flow would be early
terminated incorrectly with the context lifetime of connection 0.

The new connection context is provided during migration of a flow
and will trigger the observe loop for the flow lifetime to be rebound
to this provided context.
Closes TUN-8748
2024-11-21 12:56:47 -08:00
Chung-Ting Huang c59d56c655 Release 2024.11.1
## Summary
Some description...
2024-11-19 08:21:37 -08:00
chungthuang 3480a33fce
Merge pull request #1135 from firecow/tunnel-health
Add cloudflared tunnel ready command
2024-11-18 10:08:45 -06:00
chungthuang a26b2a0097
Merge pull request #1355 from pkillarjun/fuzzing
add: new go-fuzz targets
2024-11-18 09:53:12 -06:00
chungthuang 37eee7e727
Merge pull request #1346 from emilioarvidisaksson/patch-1
Fixed 404 in README.md to TryCloudflare
2024-11-18 09:52:44 -06:00
chungthuang 9771f3309e
Merge pull request #1329 from cloudflare/hrushikeshdeshpande-updating-semgrep-yml
Update semgrep.yml
2024-11-18 09:52:05 -06:00
Mads Jon Nielsen c39f0ae317 Make metrics a requirement for tunnel ready command 2024-11-14 18:38:17 +01:00
Mads Jon Nielsen 95dff74fc8
Merge branch 'cloudflare:master' into tunnel-health 2024-11-14 18:19:43 +01:00
Devin Carr ab3dc5f8fa TUN-8701: Simplify flow registration logs for datagram v3
To help reduce the volume of logs during the happy path of flow registration, there will only be one log message reported when a flow is completed.

There are additional fields added to all flow log messages:
1. `src`: local address
2. `dst`: origin address
3. `durationMS`: capturing the total duration of the flow in milliseconds

Additional logs were added to capture when a flow was migrated or when cloudflared sent off a registration response retry.

Closes TUN-8701
2024-11-12 10:54:37 -08:00
Arjun 53c523444e add: new go-fuzz targets
Signed-off-by: Arjun <pkillarjun@protonmail.com>
2024-11-11 20:45:49 +05:30
Devin Carr 1f3e3045ad TUN-8701: Add metrics and adjust logs for datagram v3
Closes TUN-8701
2024-11-07 11:02:55 -08:00
Devin Carr 952622a965 TUN-8709: Add session migration for datagram v3
When a registration response from cloudflared gets lost on it's way back to the edge, the edge service will retry and send another registration request. Since cloudflared already has bound the local UDP socket for the provided request id, we want to re-send the registration response.

There are three types of retries that the edge will send:

1. A retry from the same QUIC connection index; cloudflared will just respond back with a registration response and reset the idle timer for the session.
2. A retry from a different QUIC connection index; cloudflared will need to migrate the current session connection to this new QUIC connection and reset the idle timer for the session.
3. A retry to a different cloudflared connector; cloudflared will eventually time the session out since no further packets will arrive to the session at the original connector.

Closes TUN-8709
2024-11-06 12:06:07 -08:00
lneto 70393b6de4 Release 2024.11.0 2024-11-06 07:09:41 +00:00
Luis Neto e8e824a730 VULN-66059: remove ssh server tests
## Summary
The initial purpose of this PR was to bump the base image from buster to bookworm however these tests are no longer exercised hence the removal

Closes VULN-66059
2024-11-05 23:00:35 -08:00
Gonçalo Garcia 3d33f559b1 TUN-8641: Expose methods to simplify V3 Datagram parsing on the edge 2024-11-04 15:23:36 -08:00
emilioarvidisaksson aa7abe7581
Fixed 404 in README.md to TryCloudflare 2024-11-04 23:25:53 +01:00
Devin Carr 589c198d2d TUN-8646: Allow experimental feature support for datagram v3
Closes TUN-8646
2024-11-04 13:59:32 -08:00
Devin Carr 5891c0d955 TUN-8700: Add datagram v3 muxer
The datagram muxer will wrap a QUIC Connection datagram read-writer operations to unmarshal datagrams from the connection to the origin with the session manager. Incoming datagram session registration operations will create new UDP sockets for sessions to proxy UDP packets between the edge and the origin. The muxer is also responsible for marshalling UDP packets and operations into datagrams for communication over the QUIC connection towards the edge.

Closes TUN-8700
2024-11-04 11:20:35 -08:00
lneto d29017fac9 TUN-8553: Bump go to 1.22.5 and go-boring 1.22.5-1
update docker files with go1.22.5
update windows scripts with go1.22.5
2024-11-04 01:25:49 -08:00
Devin Carr 6a6c890700 TUN-8667: Add datagram v3 session manager
New session manager leverages similar functionality that was previously
provided with datagram v2, with the distinct difference that the sessions
are registered via QUIC Datagrams and unregistered via timeouts only; the
sessions will no longer attempt to unregister sessions remotely with the
edge service.

The Session Manager is shared across all QUIC connections that cloudflared
uses to connect to the edge (typically 4). This will help cloudflared be
able to monitor all sessions across the connections and help correlate
in the future if sessions migrate across connections.

The UDP payload size is still limited to 1280 bytes across all OS's. Any
UDP packet that provides a payload size of greater than 1280 will cause
cloudflared to report (as it currently does) a log error and drop the packet.

Closes TUN-8667
2024-10-31 14:05:15 -07:00
Devin Carr 599ba52750 TUN-8708: Bump python min version to 3.10
With the recent bump of the windows CI to python 3.10, we will bump the minimum required python version for component testing to 3.10.

Closes TUN-8708
2024-10-31 13:33:24 -07:00
Mads Jon Nielsen 2cbe125e0b
Merge branch 'cloudflare:master' into tunnel-health 2024-10-28 10:33:08 +01:00
Luis Neto 0eddb8a615 TUN-8692: remove dashes from session id
Closes TUN-8692
2024-10-25 05:45:24 -07:00
Devin Carr 16ecf60800 TUN-8661: Refactor connection methods to support future different datagram muxing methods
The current supervisor serves the quic connection by performing all of the following in one method:
1. Dial QUIC edge connection
2. Initialize datagram muxer for UDP sessions and ICMP
3. Wrap all together in a single struct to serve the process loops

In an effort to better support modularity, each of these steps were broken out into their own separate methods that the supervisor will compose together to create the TunnelConnection and run its `Serve` method.

This also provides us with the capability to better interchange the functionality supported by the datagram session manager in the future with a new mechanism.

Closes TUN-8661
2024-10-24 11:42:02 -07:00
Luis Neto eabc0aaaa8 TUN-8694: Rework release script
## Summary
This modifies the release script to only create the github release after verifying the assets version

Closes TUN-8694
2024-10-24 09:43:02 -07:00
GoncaloGarcia 374a920b61 Release 2024.10.1 2024-10-23 15:46:48 +01:00
lneto 6ba0c25a92 TUN-8694: Fix github release script
Remove parameter from extractall function since it does not exist in python 3.7
2024-10-23 11:08:17 +01:00
GoncaloGarcia 48f703f990 Release 2024.10.1 2024-10-22 10:08:58 +01:00
GoncaloGarcia f407dbb712 Revert "TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport"
This reverts commit e2064c820f.
2024-10-21 16:07:52 +01:00
Devin Carr 92e0f5fcf9 TUN-8688: Correct UDP bind for IPv6 edge connectivity on macOS
For macOS, we want to set the DF bit for the UDP packets used by the QUIC
connection; to achieve this, you need to explicitly set the network
to either "udp4" or "udp6". When determining which network type to pick
we need to use the edge IP address chosen to align with what the local
IP family interface we will use. This means we want cloudflared to bind
to local interfaces for a random port, so we provide a zero IP and 0 port
number (ex. 0.0.0.0:0). However, instead of providing the zero IP, we
can leave the value as nil and let the kernel decide which interface and
pick a random port as defined by the target edge IP family.

This was previously broken for IPv6-only edge connectivity on macOS and
all other operating systems should be unaffected because the network type
was left as default "udp" which will rely on the provided local or remote
IP for selection.

Closes TUN-8688
2024-10-18 14:38:05 -07:00
Devin Carr d608a64cc5 TUN-8685: Bump coredns dependency
Closes TUN-8685
2024-10-17 13:09:39 -07:00
Devin Carr abb3466c31 TUN-8638: Add datagram v3 serializers and deserializers
Closes TUN-8638
2024-10-16 12:05:55 -07:00
Devin Carr a3ee49d8a9 chore: Remove h2mux code
Some more legacy h2mux code to be cleaned up and moved out of the way.
The h2mux.Header used in the serialization for http2 proxied headers is moved to connection module. Additionally, the booleanfuse structure is also moved to supervisor as it is also needed. Both of these structures could be evaluated later for removal/updates, however, the intent of the proposed changes here is to remove the dependencies on the h2mux code and removal.

Approved-by: Chung-Ting Huang <chungting@cloudflare.com>
Approved-by: Luis Neto <lneto@cloudflare.com>
Approved-by: Gonçalo Garcia <ggarcia@cloudflare.com>

MR: https://gitlab.cfdata.org/cloudflare/tun/cloudflared/-/merge_requests/1576
2024-10-15 13:10:30 -07:00
Mads Jon Nielsen f2016e7f63
Merge branch 'cloudflare:master' into tunnel-health 2024-10-12 19:21:03 +02:00
Luis Neto bade488bdf TUN-8631: Abort release on version mismatch
Closes TUN-8631

Approved-by: Gonçalo Garcia <ggarcia@cloudflare.com>
Approved-by: Devin Carr <dcarr@cloudflare.com>

MR: https://gitlab.cfdata.org/cloudflare/tun/cloudflared/-/merge_requests/1579
2024-10-11 02:44:29 -07:00
GoncaloGarcia b426c62423 Release 2024.10.0 2024-10-10 09:56:01 +01:00
Mads Jon Nielsen 4ce0e1bd38
Merge branch 'cloudflare:master' into tunnel-health 2024-10-07 18:38:43 +02:00
GoncaloGarcia fe7ff6cbfe TUN-8621: Fix cloudflared version in change notes to account for release date 2024-10-07 10:51:21 -05:00
chungthuang e2064c820f TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport
If the metadata is missing, fallback to decide based on protocol, http
method, transferring and content length
2024-10-07 10:51:21 -05:00
GoncaloGarcia 318488e229 TUN-8484: Print response when QuickTunnel can't be unmarshalled 2024-10-07 10:51:21 -05:00
GoncaloGarcia e251a21810 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering
Whenever cloudflared receives a SIGTERM or SIGINT it goes into graceful shutdown mode, which unregisters the connection and closes the control stream. Unregistering makes it so we no longer receive any new requests and makes the edge close the connection, allowing in-flight requests to finish (within a 3 minute period).
 This was working fine for http2 connections, but the quic proxy was cancelling the context as soon as the controls stream ended, forcing the process to stop immediately.

 This commit changes the behavior so that we wait the full grace period before cancelling the request
2024-10-07 10:51:21 -05:00
Devin Carr 05249c7b51 PPIP-2310: Update quick tunnel disclaimer 2024-10-07 10:51:21 -05:00
Devin Carr d7d81384c2 TUN-8646: Add datagram v3 support feature flag 2024-10-04 12:12:54 -07:00
Hrushikesh Deshpande 659da3ebba
Update semgrep.yml 2024-09-24 21:40:50 -04:00
hrushikeshdeshpande 244248f2b7
Update semgrep.yml
Updating Semgrep.yml file - Semgrep is a tool that will be used to scan Cloudflare's public repos for Supply chain, code and secrets. This work is part of Application & Product Security team's initiative to onboard Semgrep onto all of Cloudflare's public repos.

In case of any questions, please reach out to "Hrushikesh Deshpande" on cf internal chat.
2024-09-21 13:18:55 -04:00
Hrushikesh Deshpande ea1c4a327d Adding semgrep yaml file 2024-09-19 21:52:45 -04:00
Dean Sundquist 5c5d1dc161 TUN-8629: Cloudflared update on Windows requires running it twice to update 2024-09-16 18:31:57 +00:00
Devin Carr cd8cb47866 TUN-8632: Delay checking auto-update by the provided frequency
Delaying the auto-update check timer to start after one full round of
the provided frequency reduces the chance of upgrading immediately
after starting.
2024-09-14 05:31:29 +00:00
Devin Carr 2484df1f81 TUN-8630: Check checksum of downloaded binary to compare to current for auto-updating
In the rare case that the updater downloads the same binary (validated via checksum)
we want to make sure that the updater does not attempt to upgrade and restart the cloudflared
process. The binaries are equivalent and this would provide no value.

However, we are covering this case because there was an errant deployment of cloudflared
that reported itself as an older version and was then stuck in an infinite loop
attempting to upgrade to the latest version which didn't exist. By making sure that
the binary is different ensures that the upgrade will be attempted and cloudflared
will be restarted to run the new version.

This change only affects cloudflared tunnels running with default settings or
`--no-autoupdate=false` which allows cloudflared to auto-update itself in-place. Most
distributions that handle package management at the operating system level are
not affected by this change.
2024-09-11 16:00:00 -07:00
GoncaloGarcia a57fc25b54 Release 2024.9.1 2024-09-10 17:03:43 +01:00
GoncaloGarcia 2437675c04 Reverts the following:
Revert "TUN-8621: Fix cloudflared version in change notes."
Revert "PPIP-2310: Update quick tunnel disclaimer"
Revert "TUN-8621: Prevent QUIC connection from closing before grace period after unregistering"
Revert "TUN-8484: Print response when QuickTunnel can't be unmarshalled"
Revert "TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport"
2024-09-10 16:50:32 +01:00
GoncaloGarcia ec07269122 Release 2024.9.0 2024-09-10 10:05:22 +01:00
GoncaloGarcia 3ac69f2d06 TUN-8621: Fix cloudflared version in change notes. 2024-09-10 10:01:22 +01:00
Devin Carr a29184a171 PPIP-2310: Update quick tunnel disclaimer 2024-09-06 11:33:42 -07:00
GoncaloGarcia e05939f1c9 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering
Whenever cloudflared receives a SIGTERM or SIGINT it goes into graceful shutdown mode, which unregisters the connection and closes the control stream. Unregistering makes it so we no longer receive any new requests and makes the edge close the connection, allowing in-flight requests to finish (within a 3 minute period).
 This was working fine for http2 connections, but the quic proxy was cancelling the context as soon as the controls stream ended, forcing the process to stop immediately.

 This commit changes the behavior so that we wait the full grace period before cancelling the request
2024-09-05 13:15:00 +00:00
GoncaloGarcia ab0bce58f8 TUN-8484: Print response when QuickTunnel can't be unmarshalled 2024-09-03 15:18:03 +01:00
chungthuang d6b0833209 TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport
If the metadata is missing, fallback to decide based on protocol, http
method, transferring and content length
2024-08-26 15:53:24 -05:00
chungthuang 9f0f22c036 Release 2024.8.3 2024-08-22 08:34:27 -04:00
Mads Jon Nielsen 72f8ecc521
Merge branch 'cloudflare:master' into tunnel-health 2024-08-22 07:39:56 +02:00
Kornel 394d3546bf Merge remote-tracking branch 'gh/master'
* gh/master:
  remove code that will not be executed
2024-08-20 19:02:38 +01:00
Kornel a9365296ae TUN-8591 login command without extra text
Also unifies `access token` and `access login` interface
2024-08-17 01:11:27 +01:00
sellskin 30c435fee6 remove code that will not be executed
Signed-off-by: sellskin <mydesk@yeah.net>
2024-08-07 14:31:49 +00:00
chungthuang c7d63beba2
Merge pull request #1217 from sellskin/master
remove code that will not be executed
2024-08-06 10:36:30 -05:00
lneto 9f0002db40 Release 2024.8.2 2024-08-05 18:25:12 +01:00
lneto 86f33005b9 TUN-8585: Avoid creating GH client when dry-run is true
- copy exe files from windows build
2024-08-05 17:43:58 +01:00
lneto bd9e020df9 TUN-8583: change final directory of artifacts 2024-08-05 10:49:20 +01:00
lneto b03ea055b0 TUN-8581: create dry run for github release 2024-08-01 17:42:59 +01:00
lneto ae7f7fa7e8 TUN-8546: remove call to non existant make target 2024-08-01 10:06:23 +00:00
Mads Jon Nielsen bec84aeb7b
Merge branch 'cloudflare:master' into tunnel-health 2024-08-01 10:46:25 +02:00
lneto c7f0f90bed Release 2024.7.3 2024-07-31 16:29:18 +01:00
lneto c7cd4e02b8 TUN-8546: Fix final artifacts paths
- The build artifacts must be placed in the checkout directory so that they can be picked up from cfsetup
2024-07-31 15:27:41 +00:00
lneto 3bb3d71093 Release 2024.7.2 2024-07-31 11:18:57 +01:00
lneto c2183bd814 TUN-8546: rework MacOS build script
The rework consists in building and packaging the cloudflared binary based on the OS & ARCH of the system.

read TARGET_ARCH from export and exit if TARGET_ARCH is not set
2024-07-26 10:41:47 +01:00
Mads Jon Nielsen 037f056d0c
Merge branch 'cloudflare:master' into tunnel-health 2024-07-22 16:47:55 +02:00
lneto db239e7319 Release 2024.7.1 2024-07-16 16:24:52 +01:00
lneto 26ae1ca3c8 TUN-8543: use -p flag to create intermediate directories 2024-07-16 15:21:52 +00:00
lneto 13b2e423ed Release 2024.7.0 2024-07-15 14:24:16 +01:00
lneto 47733ba25e TUN-8523: refactor makefile and cfsetup
- remove unused targets in Makefile
- order deps in cfsetup.yaml
- only build cloudflared not all linux targets
- rename stages to be more explicit
- adjust build deps of build-linux-release
- adjust build deps of build-linux-fips-release
- rename github_release_pkgs_pre_cache to build_release_pre_cache
- only build release release artifacts within build-linux-release
- only build release release artifacts within build-linux-fips-release
- remove github-release-macos
- remove github-release-windows
- adjust builddeps of test and test-fips
- create builddeps anchor for component-test and use it in component-test-fips
- remove wixl from build-linux-*
- rename release-pkgs-linux to r2-linux-release
- add github-release: artifacts uplooad and set release message
- clean build directory before build
- add step to package windows binaries
- refactor windows script
One of TeamCity changes is moving the artifacts to the built artifacts, hence, there is no need to cp files from artifacts to built_artifacts
- create anchor for release builds
- create anchor for tests stages
- remove reprepro and createrepo as they are only called by release_pkgs.py
2024-07-15 12:56:43 +01:00
lneto c95959e845 TUN-8520: add macos arm64 build
- refactor build script for macos to include arm64 build
- refactor Makefile to upload all the artifacts instead of issuing one by one
- update cfsetup due to 2.
- place build files in specific folders
- cleanup build directory before/after creating build artifacts
2024-07-11 16:23:35 +01:00
João Oliveirinha 75752b681b TUN-8057: cloudflared uses new PQ curve ID 2024-07-09 11:19:10 -07:00
Devin Carr 6174c4588b TUN-8489: Add default noop logger for capnprpc 2024-07-02 22:05:28 +00:00
Devin Carr d875839e5e TUN-8487: Add user-agent for quick-tunnel requests 2024-07-02 11:52:41 -07:00
GoncaloGarcia 1f38deca1e TUN-8504: Use pre-installed python version instead of downloading it on Windows builds
Recently python.org started blocking our requests. We've asked the Devtools team to upgrade the default python installation to 3.10 so that we can use it in our tests
2024-07-02 14:06:50 +01:00
Mads Jon Nielsen 5d229fd917
Merge branch 'cloudflare:master' into tunnel-health 2024-06-21 07:55:18 +02:00
chungthuang 628176a2d6 Release 2024.6.1 2024-06-17 10:30:52 -05:00
chungthuang 0b62d45738 TUN-8456: Update quic-go to 0.45 and collect mtu and congestion control metrics 2024-06-17 15:28:56 +00:00
chungthuang cb6e5999e1 TUN-8461: Don't log Failed to send session payload if the error is EOF 2024-06-14 14:35:18 -05:00
chungthuang a16532dbbb TUN-8451: Log QUIC flow control frames and transport parameters received 2024-06-12 19:23:39 +00:00
chungthuang 354a5bb8af TUN-8452: Add flag to control QUIC stream-level flow control limit 2024-06-06 11:50:46 -05:00
chungthuang e0b1899e97 TUN-8449: Add flag to control QUIC connection-level flow control limit and increase default to 30MB 2024-06-05 17:34:41 -05:00
Devin Carr d37ad42426 Release 2024.6.0 2024-06-03 11:29:11 -07:00
Devin Carr 44e6d1a313 TUN-8441: Correct UDP total sessions metric to a counter and add new ICMP metrics
cloudflared_udp_total_sessions was incorrectly a gauge when it
represents the total since the cloudflared process started and will
only ever increase.

Additionally adds new ICMP metrics for requests and replies.
2024-05-30 14:23:10 -07:00
Devin Carr 30197e7dfa TUN-8422: Add metrics for capnp method calls
Adds new suite of metrics to capture the following for capnp rpcs operations:
- Method calls
- Method call failures
- Method call latencies

Each of the operations is labeled by the handler that serves the method and
the method of operation invoked. Additionally, each of these are split
between if the operation was called by a client or served.
2024-05-28 14:14:25 -07:00
Devin Carr 654a326098 TUN-8424: Refactor capnp registration server
Move RegistrationServer and RegistrationClient into tunnelrpc module
to properly abstract out the capnp aspects internal to the module only.
2024-05-24 11:40:10 -07:00
Devin Carr 43446bc692 TUN-8423: Deprecate older legacy tunnel capnp interfaces
Since legacy tunnels have been removed for a while now, we can remove
many of the capnp rpc interfaces that are no longer leveraged by the
legacy tunnel registration and authentication mechanisms.
2024-05-23 11:17:49 -07:00
Devin Carr e9f010111d TUN-8425: Remove ICMP binding for quick tunnels 2024-05-23 18:16:30 +00:00
Devin Carr 8184bc457d TUN-8427: Fix BackoffHandler's internally shared clock structure
A clock structure was used to help support unit testing timetravel
but it is a globally shared object and is likely unsafe to share
across tests. Reordering of the tests seemed to have intermittent
failures for the TestWaitForBackoffFallback specifically on windows
builds.

Adjusting this to be a shim inside the BackoffHandler struct should
resolve shared object overrides in unit testing.

Additionally, added the reset retries functionality to be inline with
the ResetNow function of the BackoffHandler to align better with
expected functionality of the method.

Removes unused reconnectCredentialManager.
2024-05-23 09:48:34 -07:00
Mads Jon Nielsen e759716ce7
Merge branch 'cloudflare:master' into tunnel-health 2024-05-21 09:09:53 +02:00
Devin Carr 2db00211f5 TUN-8419: Add capnp safe transport
To help support temporary errors that can occur in the capnp rpc
calls, a wrapper is introduced to inspect the error conditions and
allow for retrying within a short window.
2024-05-19 20:34:32 -07:00
Devin Carr eb2e4349e8 TUN-8415: Refactor capnp rpc into a single module
Combines the tunnelrpc and quic/schema capnp files into the same module.

To help reduce future issues with capnp id generation, capnpids are
provided in the capnp files from the existing capnp struct ids generated
in the go files.

Reduces the overall interface of the Capnp methods to the rest of
the code by providing an interface that will handle the quic protocol
selection.

Introduces a new `rpc-timeout` config that will allow all of the
SessionManager and ConfigurationManager RPC requests to have a timeout.
The timeout for these values is set to 5 seconds as non of these operations
for the managers should take a long time to complete.

Removed the RPC-specific logger as it never provided good debugging value
as the RPC method names were not visible in the logs.
2024-05-17 11:22:07 -07:00
João "Pisco" Fernandes 7d76ce2d24 Release 2024.5.0 2024-05-16 15:20:06 +01:00
João "Pisco" Fernandes 66efd3f2bb TUN-8407: Upgrade go to version 1.22.2 2024-05-07 16:58:57 +01:00
Mads Jon Nielsen 2941825577 Make sure body is properly printed when status code not equals 200 2024-04-23 09:00:39 +02:00
Mads Jon Nielsen d094e52bd1 Prettify Usage and Description 2024-04-23 08:53:49 +02:00
Mads Jon Nielsen d6b03fbabf Prettify Usage and Description 2024-04-23 08:53:32 +02:00
Mads Jon Nielsen e03f53144b Rename command description and usage 2024-04-23 08:37:37 +02:00
Mads Jon Nielsen b342c7403c Rename command to ready 2024-04-23 08:35:25 +02:00
Mads Jon Nielsen 37210ff661 Use /healthcheck over /ready 2024-04-23 08:20:53 +02:00
Mads Jon Nielsen 6db3cb2f1b
Merge branch 'cloudflare:master' into tunnel-health 2024-04-23 08:08:04 +02:00
chungthuang f27418044b Release 2024.4.1 2024-04-22 17:16:50 -05:00
Devin Carr 1b02d169ad TUN-8374: Close UDP socket if registration fails
If cloudflared was unable to register the UDP session with the
edge, the socket would be left open to be eventually closed by the
OS, or garbage collected by the runtime. Considering that either of
these closes happened significantly after some delay, it was causing
cloudflared to hold open file descriptors longer than usual if continuously
unable to register sessions.
2024-04-22 21:59:43 +00:00
João "Pisco" Fernandes 84833011ec TUN-8371: Bump quic-go to v0.42.0
## Summary
We discovered that we were being impacted by a bug in quic-go,
that could create deadlocks and not close connections.

This commit bumps quic-go to the version that contains the fix
to prevent that from happening.
2024-04-22 14:48:49 -05:00
chungthuang 5e5f2f4d8c TUN-8380: Add sleep before requesting quick tunnel as temporary fix for component tests 2024-04-22 13:50:04 -05:00
Devin Carr b9898a9fbe TUN-8331: Add unit testing for AccessJWTValidator middleware 2024-04-11 12:25:24 -07:00
Devin Carr 687682120c TUN-8333: Bump go-jose dependency to v4 2024-04-10 09:49:40 -07:00
Devin Carr a1a9f3813e Release 2024.4.0 2024-04-08 14:09:14 -07:00
GoncaloGarcia 7deb4340b4 Format code 2024-04-02 14:58:05 -07:00
Steven Kreitzer b5be8a6fa4 feat: auto tls sni
Signed-off-by: Steven Kreitzer <skre@skre.me>
2024-04-02 14:56:44 -07:00
Alexandru Tocar a665d3245a
feat: provide short version (#1206)
Provides a short version output to assist with CLI parsing.
---------

Co-authored-by: Alex Tocar <alex.tocar@ueuie.dev>
2024-04-02 08:31:18 -07:00
chungthuang a48691fe78
Merge pull request #1125 from Shakahs/master
[access] Add environment variables for TCP tunnel hostname / destination / URL.
2024-04-02 10:25:21 -05:00
chungthuang b723a1a426
Merge pull request #1130 from crrodriguez/checkInPingGroupBugs
fix checkInPingGroup bugs
2024-04-02 10:24:51 -05:00
sellskin 619c12cc64 remove code that will not be executed
Signed-off-by: sellskin <mydesk@yeah.net>
2024-03-25 12:53:53 +08:00
GoncaloGarcia bb29a0e194 Release 2024.3.0 2024-03-19 18:08:31 +00:00
GoncaloGarcia 86476e6248 TUN-8281: Run cloudflared query list tunnels/routes endpoint in a paginated way
Before this commit the commands that listed tunnels and tunnel routes would be limited to 1000 results by the server.

Now, the commands will call the endpoints until the result set is exhausted. This can take a long time if there are
thousands of pages available, since each request is executed synchronously.
From a user's perspective, nothing changes.
2024-03-19 16:35:40 +00:00
João "Pisco" Fernandes da6fac4133 TUN-8297: Improve write timeout logging on safe_stream.go
## Summary:
In order to properly monitor what is happening with the new write timeouts that we introduced
in TUN-8244 we need proper logging. Right now we were logging write timeouts when the safe
stream was being closed which didn't make sense because it was miss leading, so this commit
prevents that by adding a flag that allows us to know whether we are closing the stream or not.
2024-03-13 13:30:45 +00:00
João "Pisco" Fernandes 47ad3238dd TUN-8290: Remove `|| true` from postrm.sh 2024-03-07 16:22:56 +00:00
João "Pisco" Fernandes 4f7165530c TUN-8275: Skip write timeout log on "no network activity"
## Summary
To avoid having to verbose logs we need to only log when an
actual issue occurred. Therefore, we will be skipping any error
logging if the write timeout is caused by no network activity
which just means that nothing is being sent through the stream.
2024-03-06 16:05:48 +00:00
Nikita Sivukhin a36fa07aba fix typo in errcheck for response parsing logic in CreateTunnel routine 2024-03-06 10:29:55 +00:00
Nanashi e846943e66 Update postrm.sh to fix incomplete uninstall 2024-03-06 10:29:55 +00:00
YueYue 652c82daa9 Update linux_service.go
Fix service fail to start due to unavaliable network
2024-03-06 10:29:55 +00:00
K.B.Dharun Krishna a6760a6cbf ci/check: bump actions/setup-go to v5 2024-03-06 10:29:55 +00:00
K.B.Dharun Krishna 204d55ecec ci: bump actions/checkout to v4 2024-03-06 10:29:55 +00:00
K.B.Dharun Krishna 1f4511ca6e check.yaml: bump actions/setup-go to v4 2024-03-06 10:29:55 +00:00
chungthuang 110b2b4c80 Release 2024.2.1 2024-02-20 16:25:25 +00:00
João Oliveirinha dc2c76738a TUN-8242: Update Changes.md file with new remote diagnostics behaviour 2024-02-20 16:22:20 +00:00
João Oliveirinha 5344a0bc6a TUN-8242: Enable remote diagnostics by default
This commit makes the remote diagnostics enabled by default, which is
a useful feature when debugging cloudflared issues without manual intervention from users.
Users can still opt-out by disabling the feature flag.
2024-02-20 11:31:16 +00:00
chungthuang 3299a9bc15 TUN-8238: Fix type mismatch introduced by fast-forward 2024-02-19 12:41:38 +00:00
chungthuang 34a876e4e7 TUN-8243: Collect metrics on the number of QUIC frames sent/received
This commit also removed the server metrics that is no longer used
2024-02-19 10:09:14 +00:00
Devin Carr 971360d5e0 TUN-8238: Refactor proxy logging
Propagates the logger context into further locations to help provide more context for certain errors. For instance, upstream and downstream copying errors will properly have the assigned flow id attached and destination address.
2024-02-16 20:12:24 +00:00
João "Pisco" Fernandes 76badfa01b TUN-8236: Add write timeout to quic and tcp connections
## Summary
To prevent bad eyeballs and severs to be able to exhaust the quic
control flows we are adding the possibility of having a timeout
for a write operation to be acknowledged. This will prevent hanging
connections from exhausting the quic control flows, creating a DDoS.
2024-02-15 17:54:52 +00:00
Igor Postelnik 56aeb6be65 TUN-8224: Fix safety of TCP stream logging, separate connect and ack log messages 2024-02-09 09:56:56 -06:00
chungthuang a9aa48d7a1 Release 2024.2.0 2024-02-08 10:20:02 +00:00
chungthuang 638203f9f1 TUN-8224: Count and collect metrics on stream connect successes/errors 2024-02-07 14:38:21 +00:00
chungthuang 98e043d17d Release 2024.1.5 2024-01-25 16:29:22 +00:00
João Oliveirinha 3ad4b732d4 TUN-8176: Support ARM platforms that don't have an FPU or have it enabled in kernel 2024-01-22 16:35:59 +00:00
chungthuang 9c1f5c33a8 TUN-8158: Bring back commit e653741885 and fixes infinite loop on linux when the socket is closed 2024-01-22 13:46:33 +00:00
chungthuang f75503bf3c Release 2024.1.4 2024-01-19 19:42:57 +00:00
chungthuang 2c38487a54 Revert "TUN-8158: Add logging to confirm when ICMP reply is returned to the edge"
This reverts commit e653741885.
2024-01-19 19:37:28 +00:00
chungthuang ae0b261e56 Release 2024.1.3 2024-01-16 15:58:24 +00:00
chungthuang e653741885 TUN-8158: Add logging to confirm when ICMP reply is returned to the edge 2024-01-16 15:56:24 +00:00
João Oliveirinha e5ae80ab86 TUN-8161: Fix broken ARM build for armv6
During the recent changes to the build pipeline, the implicit GOARM env variable changed from
6 to 7.
This means we need to explicitly define the GOARM to v6.
2024-01-16 09:58:39 +00:00
chungthuang ba2edca352 Release 2024.1.2 2024-01-11 16:24:27 +00:00
Chung-Ting c8ffdae859 TUN-8146: Fix Makefile targets should not be run in parallel and install-go script was missing shebang 2024-01-11 15:36:15 +00:00
Chung-Ting 8fc8c17522 TUN-8146: Fix export path for install-go command
This should fix homebrew-core to use the correct go tool chain
2024-01-11 12:38:28 +00:00
João "Pisco" Fernandes 8d9aab5217 TUN-8140: Remove homebrew scripts
## Summary
We have decided to no longer push cloudflared to cloudflare homebrew, and use
the automation from homebrew-core to update cloudflared on their repository.
Therefore, the scripts for homebrew and makefile targets are no longer necessary.
2024-01-11 11:34:33 +00:00
João Oliveirinha 25f91fec10 TUN-8147: Disable ECN usage due to bugs in detecting if supported 2024-01-11 10:35:25 +00:00
chungthuang c7b2cce131 Release 2024.1.1 2024-01-10 12:14:10 +00:00
chungthuang 3e5c2959db TUN-8134: Revert installed prefix to /usr 2024-01-10 11:43:55 +00:00
chungthuang 37ec2d4830 TUN-8134: Install cloudflare go as part of make install
To build cloudflared from source, one will need a go tool chain that
supports post quantum curves
2024-01-10 10:23:43 +00:00
chungthuang ecd101d485 TUN-8130: Install go tool chain in /tmp on build agents 2024-01-09 22:50:05 +00:00
chungthuang cf5be91d2d TUN-8129: Use the same build command between branch and release builds 2024-01-09 17:07:49 +00:00
chungthuang 28685a5055 TUN-8130: Fix path to install go for mac build 2024-01-09 12:33:41 +00:00
chungthuang e23d928829 TUN-8118: Disable FIPS module to build with go-boring without CGO_ENABLED 2024-01-08 18:16:06 +00:00
chungthuang 159fcb44ce Release 2024.1.0 2024-01-03 17:25:47 +00:00
chungthuang 8e69f41833 TUN-7934: Update quic-go to a version that queues datagrams for better throughput and drops large datagram
Remove TestUnregisterUdpSession
2024-01-03 13:01:01 +00:00
Mads Jon Nielsen 2bf652c6fd
Update subcommands.go 2024-01-03 10:38:55 +01:00
Mads Jon Nielsen 521f5632d7
Just use c.String("metrics")
Co-authored-by: Julien Laffaye <jlaffaye@freebsd.org>
2024-01-03 10:37:41 +01:00
Mads Jon Nielsen f10247db90 Add cloudflared tunnel health command 2023-12-28 19:50:49 +01:00
Cristian Rodríguez fbe357b1e6 fix checkInPingGroup bugs
- Must check for the *effective* GID.
- Must allow range from  0 to 4294967294 in current kernels.
2023-12-24 14:04:55 -03:00
chungthuang 00cd7c333c TUN-8072: Need to set GOCACHE in mac go installation script 2023-12-20 05:28:13 +00:00
chungthuang 86b50eda15 TUN-8072: Add script to download cloudflare go for Mac build agents 2023-12-19 22:36:48 +00:00
James Royal 652df22831 AUTH-5682 Org token flow in Access logins should pass CF_AppSession cookie
- Refactor HandleRedirects function and add unit tests
- Move signal test to its own file because of OS specific instructions
2023-12-18 09:42:33 -06:00
Shak Saleemi 1776d3d335
Add environment variables for TCP tunnel hostname / destination / URL. 2023-12-15 16:02:36 -08:00
Chung-Ting 33baad35b8 TUN-8066: Define scripts to build on Windows agents 2023-12-15 23:21:42 +00:00
Chung-Ting 12dd91ada1 TUN-8052: Update go to 1.21.5
Also update golang.org/x/net and google.golang.org/grpc to fix vulnerabilities,
although cloudflared is using them in a way that is not exposed to those risks
2023-12-15 12:17:21 +00:00
Honahuku b901d73d9b
configuration.go: fix developerPortal link (#960) 2023-12-14 16:34:00 +00:00
Kyle Carberry 61a16538a1
Use CLI context when running tunnel (#597)
When embedding the tunnel command inside another CLI, it
became difficult to test shutdown behavior due to this leaking
tunnel. By using the command context, we're able to shutdown
gracefully.
2023-12-14 16:33:41 +00:00
TMKnight 9e1f4c2bca
Remove extraneous `period` from Path Environment Variable (#1009) 2023-12-14 16:32:48 +00:00
Alex Vanderpot f51be82729
use os.Executable to discover the path to cloudflared (#1040) 2023-12-14 16:32:31 +00:00
Lars Lehtonen fd5d8260bb
cmd/cloudflared/updater: fix dropped error (#1055) 2023-12-14 16:31:47 +00:00
Sam Cook f2c4fdb0ae
Fix nil pointer dereference segfault when passing "null" config json to cloudflared tunnel ingress validate (#1070) 2023-12-14 16:29:40 +00:00
Lars Lehtonen a4a84bb27e
tunnelrpc/pogs: fix dropped test errors (#1106) 2023-12-14 16:29:16 +00:00
Chung-Ting 4ddc8d758b TUN-7970: Default to enable post quantum encryption for quic transport 2023-12-07 11:37:46 +00:00
Chung-Ting 8068cdebb6 TUN-8006: Update quic-go to latest upstream 2023-12-04 17:09:40 +00:00
James Royal 45236a1f7d VULN-44842 Add a flag that allows users to not send the Access JWT to stdout 2023-11-16 11:45:37 -06:00
Devin Carr e0a55f9c0e TUN-7965: Remove legacy incident status page check 2023-11-13 17:10:59 -08:00
Sudarsan Reddy c1d8c5e960 Release 2023.10.0 2023-10-31 09:11:23 +00:00
Devin Carr 7ae1d4668e TUN-7864: Document cloudflared versions support 2023-10-06 11:30:59 -07:00
João Oliveirinha adb7d40084 CUSTESC-33731: Make rule match test report rule in 0-index base
This changes guarantees that the coommand to report rule matches when
testing local config reports the rule number using the 0-based indexing.
This is to be consistent with the 0-based indexing on the log lines when
proxying requests.
2023-10-03 12:18:49 +01:00
João "Pisco" Fernandes 541c63d737 TUN-7824: Fix usage of systemctl status to detect which services are installed
## Summary
To determine which services were installed, cloudflared, was using the command
`systemctl status` this command gives an error if the service is installed
but isn't running, which makes the `uninstall services` command report wrongly
the services not installed. Therefore, this commit adapts it to use the
`systemctl list-units` command combined with a grep to find which services are
installed and need to be removed.
2023-09-22 15:35:55 +01:00
João Oliveirinha f1d6f0c0be TUN-7787: cloudflared only list ip routes targeted for cfd_tunnel 2023-09-20 16:05:50 +00:00
João "Pisco" Fernandes 958b6f1d24 TUN-7813: Improve tunnel delete command to use cascade delete
## Summary
Previously the force flag in the tunnel delete command was only explicitly deleting the
connections of a tunnel. Therefore, we are changing it to use the cascade query parameter
supported by the API. That parameter will delegate to the server the deletion of the tunnel
dependencies implicitly instead of the client doing it explicitly. This means that not only
the connections will get deleted, but also the tunnel routes, ensuring that no dependencies
are left without a non-deleted tunnel.
2023-09-20 12:35:43 +01:00
João Oliveirinha 6d1d91d9f9 TUN-7787: Refactor cloudflared to use new route endpoints based on route IDs
This commits makes sure that cloudflared starts using the new API
endpoints for managing routes.

Additionally, the delete route operation still allows deleting by CIDR
and VNet but it is being marked as deprecated in favor of specifying the
route ID.

The goal of this change is to make it simpler for the user to delete
routes without specifying Vnet.
2023-09-19 09:56:02 +00:00
João Oliveirinha fc0ecf4185 TUN-7776: Remove warp-routing flag from cloudflared 2023-09-18 10:02:56 +01:00
João Oliveirinha 349586007c TUN-7756: Clarify that QUIC is mandatory to support ICMP proxying 2023-09-05 15:58:19 +01:00
Chung-Ting Huang 569a7c3c9e Release 2023.8.2 2023-08-30 16:39:52 +01:00
Chung-Ting Huang bec683b67d TUN-7700: Implement feature selector to determine if connections will prefer post quantum cryptography 2023-08-29 09:05:33 +01:00
Chung-Ting Huang 38d3c3cae5 TUN-7707: Use X25519Kyber768Draft00 curve when post-quantum feature is enabled 2023-08-28 14:18:05 +00:00
Chung-Ting Huang f2d765351d Release 2023.8.1 2023-08-25 16:39:08 +01:00
Sudarsan Reddy 5d8f60873d TUN-7718: Update R2 Token to no longer encode secret
This is simply because we no longer use the legacy R2 secret that needed
this encoding.
2023-08-25 13:01:28 +00:00
Chung-Ting Huang b474778cf1 Release 2023.8.0 2023-08-23 10:28:23 +01:00
Devin Carr 65247b6f0f TUN-7584: Bump go 1.20.6
Pins all docker and cfsetup builds to a specific go patch version.
Also ran go fix on repo.
2023-07-26 13:52:40 -07:00
Devin Carr 5f3cfe044f Release 2023.7.3 2023-07-25 13:51:49 -07:00
Devin Carr 81fe0bd12b TUN-7628: Correct Host parsing for Access
Will no longer provide full hostname with path from provided
`--hostname` flag for cloudflared access to the Host header field.
This addresses certain issues caught from a security fix in go
1.19.11 and 1.20.6 in the net/http URL parsing.
2023-07-25 09:33:11 -07:00
João Oliveirinha bfeaa3418d TUN-7624: Fix flaky TestBackoffGracePeriod test in cloudflared 2023-07-24 14:39:25 +01:00
2080 changed files with 154062 additions and 114352 deletions

View File

@ -4,15 +4,15 @@ jobs:
check: check:
strategy: strategy:
matrix: matrix:
go-version: [1.19.x] go-version: [1.22.x]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Test - name: Test
run: make test run: make test

24
.github/workflows/semgrep.yml vendored Normal file
View File

@ -0,0 +1,24 @@
on:
pull_request: {}
workflow_dispatch: {}
push:
branches:
- main
- master
schedule:
- cron: '0 0 * * *'
name: Semgrep config
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-latest
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
SEMGREP_URL: https://cloudflare.semgrep.dev
SEMGREP_APP_URL: https://cloudflare.semgrep.dev
SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v4
- run: semgrep ci

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ cscope.*
ssh_server_tests/.env ssh_server_tests/.env
/.cover /.cover
built_artifacts/ built_artifacts/
component-tests/.venv

131
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,131 @@
stages: [build, release]
default:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.cfdata.org
# This before_script is injected into every job that runs on master meaning that if there is no tag the step
# will succeed but only write "No tag present - Skipping" to the console.
.check_tag:
before_script:
- |
# Check if there is a Git tag pointing to HEAD
echo "Tag found: $(git tag --points-at HEAD | grep .)"
if git tag --points-at HEAD | grep .; then
echo "Tag found: $(git tag --points-at HEAD | grep .)"
export "VERSION=$(git tag --points-at HEAD | grep .)"
else
echo "No tag present — skipping."
exit 0
fi
## A set of predefined rules to use on the different jobs
.default_rules:
# Rules to run the job only on the master branch
run_on_master:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: always
- when: never
# Rules to run the job only on branches that are not master. This is needed because for now
# we need to keep a similar behavior due to the integration with teamcity, which requires us
# to not trigger pipelines on tags and/or merge requests.
run_on_branch:
- if: $CI_COMMIT_TAG
when: never
- if: $CI_PIPELINE_SOURCE != "merge_request_event" && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
when: always
- when: never
# -----------------------------------------------
# Stage 1: Build on every PR
# -----------------------------------------------
build_cloudflared_macos: &build
stage: build
rules:
- !reference [.default_rules, run_on_branch]
tags:
- "macstadium-${RUNNER_ARCH}"
parallel:
matrix:
- RUNNER_ARCH: [arm, intel]
artifacts:
paths:
- artifacts/*
script:
- '[ "${RUNNER_ARCH}" = "arm" ] && export TARGET_ARCH=arm64'
- '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64'
- ARCH=$(uname -m)
- echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH
- ./.teamcity/mac/install-cloudflare-go.sh
- export PATH="/tmp/go/bin:$PATH"
- BUILD_SCRIPT=.teamcity/mac/build.sh
- if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi
- set -euo pipefail
- echo "Executing ${BUILD_SCRIPT}"
- exec ${BUILD_SCRIPT}
# -----------------------------------------------
# Stage 1: Build and sign only on releases
# -----------------------------------------------
build_and_sign_cloudflared_macos:
<<: *build
rules:
- !reference [.default_rules, run_on_master]
secrets:
APPLE_DEV_CA_CERT:
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/apple_dev_ca_cert_v2/data@kv
file: false
CFD_CODE_SIGN_CERT:
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_cert_v2/data@kv
file: false
CFD_CODE_SIGN_KEY:
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_key_v2/data@kv
file: false
CFD_CODE_SIGN_PASS:
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_pass_v2/data@kv
file: false
CFD_INSTALLER_CERT:
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_cert_v2/data@kv
file: false
CFD_INSTALLER_KEY:
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_key_v2/data@kv
file: false
CFD_INSTALLER_PASS:
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_pass_v2/data@kv
file: false
# -----------------------------------------------
# Stage 2: Release to Github after building and signing
# -----------------------------------------------
release_cloudflared_macos_to_github:
stage: release
image: docker-registry.cfdata.org/stash/tun/docker-images/cloudflared-ci/main:6-8616fe631b76-amd64@sha256:96f4fd05e66cec03e0864c1bcf09324c130d4728eef45ee994716da499183614
extends: .check_tag
dependencies:
- build_and_sign_cloudflared_macos
rules:
- !reference [.default_rules, run_on_master]
cache:
paths:
- .cache/pip
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
KV_NAMESPACE: 380e19aa04314648949b6ad841417ebe
KV_ACCOUNT: 5ab4e9dfbd435d24068829fda0077963
secrets:
KV_API_TOKEN:
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_kv_api_token/data@kv
file: false
API_KEY:
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_github_api_key/data@kv
file: false
script:
- python3 --version ; pip --version # For debugging
- python3 -m venv venv
- source venv/bin/activate
- pip install pynacl==1.4.0 pygithub==1.55
- echo $VERSION
- echo $TAG_EXISTS
- echo "Running release because tag exists."
- make macos-release

89
.golangci.yaml Normal file
View File

@ -0,0 +1,89 @@
linters:
enable:
# Some of the linters below are commented out. We should uncomment and start running them, but they return
# too many problems to fix in one commit. Something for later.
- asasalint # Check for pass []any as any in variadic func(...any).
- asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name.
- bidichk # Checks for dangerous unicode character sequences.
- bodyclose # Checks whether HTTP response body is closed successfully.
- decorder # Check declaration order and count of types, constants, variables and functions.
- dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()).
- dupl # Tool for code clone detection.
- dupword # Checks for duplicate words in the source code.
- durationcheck # Check for two durations multiplied together.
- 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.
- misspell # Finds commonly misspelled English words.
- prealloc # Finds slice declarations that could potentially be pre-allocated.
- promlinter # Check Prometheus metrics naming via promlint.
- sloglint # Ensure consistent code style when using log/slog.
- sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed.
- staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary.
- tenv # Tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17.
- testableexamples # Linter checks if examples are testable (have an expected output).
- testifylint # Checks usage of github.com/stretchr/testify.
- tparallel # Tparallel detects inappropriate usage of t.Parallel() method in your Go test codes.
- unconvert # Remove unnecessary type conversions.
- unused # Checks Go code for unused constants, variables, functions and types.
- 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/
run:
timeout: 5m
modules-download-mode: vendor
# output configuration options
output:
formats:
- format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
issues:
# Maximum issues count per one linter.
# Set to 0 to disable.
# Default: 50
max-issues-per-linter: 50
# Maximum count of issues with the same text.
# Set to 0 to disable.
# Default: 3
max-same-issues: 15
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing large codebase.
# It's not practical to fix all existing issues at the moment of integration:
# much better don't allow issues in new code.
#
# Default: false
new: true
# Show only new issues created after git revision `REV`.
# Default: ""
new-from-rev: ac34f94d423273c8fa8fdbb5f2ac60e55f2c77d5
# Show issues in any part of update files (requires new-from-rev or new-from-patch).
# Default: false
whole-files: true
# Which dirs to exclude: issues from them won't be reported.
# Can use regexp here: `generated.*`, regexp is applied on full path,
# including the path prefix if one is set.
# Default dirs are skipped independently of this option's value (see exclude-dirs-use-default).
# "/" will be replaced by current OS file path separator to properly work on Windows.
# Default: []
exclude-dirs:
- vendor
linters-settings:
# Check exhaustiveness of enum switch statements.
exhaustive:
# Presence of "default" case in switch statements satisfies exhaustiveness,
# even if all enum members are not listed.
# Default: false
default-signifies-exhaustive: true

View File

@ -1,184 +0,0 @@
#!/bin/bash
set -exo pipefail
if [[ "$(uname)" != "Darwin" ]] ; then
echo "This should be run on macOS"
exit 1
fi
go version
export GO111MODULE=on
# build 'cloudflared-darwin-amd64.tgz'
mkdir -p artifacts
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
PKGNAME="$(pwd)/artifacts/cloudflared-amd64.pkg"
TARGET_DIRECTORY=".build"
BINARY_NAME="cloudflared"
VERSION=$(git describe --tags --always --dirty="-dev")
PRODUCT="cloudflared"
CODE_SIGN_PRIV="code_sign.p12"
CODE_SIGN_CERT="code_sign.cer"
INSTALLER_PRIV="installer.p12"
INSTALLER_CERT="installer.cer"
BUNDLE_ID="com.cloudflare.cloudflared"
SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain."
export PATH="$PATH:/usr/local/bin"
mkdir -p ../src/github.com/cloudflare/
cp -r . ../src/github.com/cloudflare/cloudflared
cd ../src/github.com/cloudflare/cloudflared
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
# Add code signing private key to the key chain
if [[ ! -z "$CFD_CODE_SIGN_KEY" ]]; then
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
# write private key to disk and then import it keychain
echo -n -e ${CFD_CODE_SIGN_KEY} | base64 -D > ${CODE_SIGN_PRIV}
# we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error
# will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below.
out=$(security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}" 2>&1) || true
exitcode=$?
if [ -n "$out" ]; then
if [ $exitcode -eq 0 ]; then
echo "$out"
else
if [ "$out" != "${SEC_DUP_MSG}" ]; then
echo "$out" >&2
exit $exitcode
fi
fi
fi
rm ${CODE_SIGN_PRIV}
fi
fi
# Add code signing certificate to the key chain
if [[ ! -z "$CFD_CODE_SIGN_CERT" ]]; then
# write certificate to disk and then import it keychain
echo -n -e ${CFD_CODE_SIGN_CERT} | base64 -D > ${CODE_SIGN_CERT}
out1=$(security import ${CODE_SIGN_CERT} -A 2>&1) || true
exitcode1=$?
if [ -n "$out1" ]; then
if [ $exitcode1 -eq 0 ]; then
echo "$out1"
else
if [ "$out1" != "${SEC_DUP_MSG}" ]; then
echo "$out1" >&2
exit $exitcode1
else
echo "already imported code signing certificate"
fi
fi
fi
rm ${CODE_SIGN_CERT}
fi
# Add package signing private key to the key chain
if [[ ! -z "$CFD_INSTALLER_KEY" ]]; then
if [[ ! -z "$CFD_INSTALLER_PASS" ]]; then
# write private key to disk and then import it into the keychain
echo -n -e ${CFD_INSTALLER_KEY} | base64 -D > ${INSTALLER_PRIV}
out2=$(security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}" 2>&1) || true
exitcode2=$?
if [ -n "$out2" ]; then
if [ $exitcode2 -eq 0 ]; then
echo "$out2"
else
if [ "$out2" != "${SEC_DUP_MSG}" ]; then
echo "$out2" >&2
exit $exitcode2
fi
fi
fi
rm ${INSTALLER_PRIV}
fi
fi
# Add package signing certificate to the key chain
if [[ ! -z "$CFD_INSTALLER_CERT" ]]; then
# write certificate to disk and then import it keychain
echo -n -e ${CFD_INSTALLER_CERT} | base64 -D > ${INSTALLER_CERT}
out3=$(security import ${INSTALLER_CERT} -A 2>&1) || true
exitcode3=$?
if [ -n "$out3" ]; then
if [ $exitcode3 -eq 0 ]; then
echo "$out3"
else
if [ "$out3" != "${SEC_DUP_MSG}" ]; then
echo "$out3" >&2
exit $exitcode3
else
echo "already imported installer certificate"
fi
fi
fi
rm ${INSTALLER_CERT}
fi
# get the code signing certificate name
if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then
CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
else
if [[ -n "$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
else
CODE_SIGN_NAME=""
fi
fi
# get the package signing certificate name
if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then
PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
else
if [[ -n "$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
else
PKG_SIGN_NAME=""
fi
fi
# sign the cloudflared binary
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
# notarize the binary
# TODO: https://jira.cfdata.org/browse/TUN-5789
fi
# creating build directory
rm -rf $TARGET_DIRECTORY
mkdir "${TARGET_DIRECTORY}"
mkdir "${TARGET_DIRECTORY}/contents"
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
# copy cloudflared into the build directory
cp ${BINARY_NAME} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
# compress cloudflared into a tar and gzipped file
tar czf "$FILENAME" "${BINARY_NAME}"
# build the installer package
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
--sign "${PKG_SIGN_NAME}" \
${PKGNAME}
# notarize the package
# TODO: https://jira.cfdata.org/browse/TUN-5789
else
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PKGNAME}
fi
# cleaning up the build directory
rm -rf $TARGET_DIRECTORY

8
.teamcity/install-cloudflare-go.sh vendored Executable file
View File

@ -0,0 +1,8 @@
# !/usr/bin/env bash
cd /tmp
git clone -q https://github.com/cloudflare/go
cd go/src
# https://github.com/cloudflare/go/tree/af19da5605ca11f85776ef7af3384a02a315a52b is version go1.22.5-devel-cf
git checkout -q af19da5605ca11f85776ef7af3384a02a315a52b
./make.bash

228
.teamcity/mac/build.sh vendored Executable file
View File

@ -0,0 +1,228 @@
#!/bin/bash
set -exo pipefail
if [[ "$(uname)" != "Darwin" ]] ; then
echo "This should be run on macOS"
exit 1
fi
if [[ "amd64" != "${TARGET_ARCH}" && "arm64" != "${TARGET_ARCH}" ]]
then
echo "TARGET_ARCH must be amd64 or arm64"
exit 1
fi
go version
export GO111MODULE=on
# build 'cloudflared-darwin-amd64.tgz'
mkdir -p artifacts
TARGET_DIRECTORY=".build"
BINARY_NAME="cloudflared"
VERSION=$(git describe --tags --always --dirty="-dev")
PRODUCT="cloudflared"
APPLE_CA_CERT="apple_dev_ca.cert"
CODE_SIGN_PRIV="code_sign.p12"
CODE_SIGN_CERT="code_sign.cer"
INSTALLER_PRIV="installer.p12"
INSTALLER_CERT="installer.cer"
BUNDLE_ID="com.cloudflare.cloudflared"
SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain."
export PATH="$PATH:/usr/local/bin"
FILENAME="$(pwd)/artifacts/cloudflared-darwin-$TARGET_ARCH.tgz"
PKGNAME="$(pwd)/artifacts/cloudflared-$TARGET_ARCH.pkg"
mkdir -p ../src/github.com/cloudflare/
cp -r . ../src/github.com/cloudflare/cloudflared
cd ../src/github.com/cloudflare/cloudflared
# Imports certificates to the Apple KeyChain
import_certificate() {
local CERTIFICATE_NAME=$1
local CERTIFICATE_ENV_VAR=$2
local CERTIFICATE_FILE_NAME=$3
echo "Importing $CERTIFICATE_NAME"
if [[ ! -z "$CERTIFICATE_ENV_VAR" ]]; then
# write certificate to disk and then import it keychain
echo -n -e ${CERTIFICATE_ENV_VAR} | base64 -D > ${CERTIFICATE_FILE_NAME}
# we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error
# will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below.
local out=$(security import ${CERTIFICATE_FILE_NAME} -T /usr/bin/pkgbuild -A 2>&1) || true
local exitcode=$?
# delete the certificate from disk
rm -rf ${CERTIFICATE_FILE_NAME}
if [ -n "$out" ]; then
if [ $exitcode -eq 0 ]; then
echo "$out"
else
if [ "$out" != "${SEC_DUP_MSG}" ]; then
echo "$out" >&2
exit $exitcode
else
echo "already imported code signing certificate"
fi
fi
fi
fi
}
create_cloudflared_build_keychain() {
# Reusing the private key password as the keychain key
local PRIVATE_KEY_PASS=$1
# Create keychain only if it doesn't already exist
if [ ! -f "$HOME/Library/Keychains/cloudflared_build_keychain.keychain-db" ]; then
security create-keychain -p "$PRIVATE_KEY_PASS" cloudflared_build_keychain
else
echo "Keychain already exists: cloudflared_build_keychain"
fi
# Append temp keychain to the user domain
security list-keychains -d user -s cloudflared_build_keychain $(security list-keychains -d user | sed s/\"//g)
# Remove relock timeout
security set-keychain-settings cloudflared_build_keychain
# Unlock keychain so it doesn't require password
security unlock-keychain -p "$PRIVATE_KEY_PASS" cloudflared_build_keychain
}
# Imports private keys to the Apple KeyChain
import_private_keys() {
local PRIVATE_KEY_NAME=$1
local PRIVATE_KEY_ENV_VAR=$2
local PRIVATE_KEY_FILE_NAME=$3
local PRIVATE_KEY_PASS=$4
echo "Importing $PRIVATE_KEY_NAME"
if [[ ! -z "$PRIVATE_KEY_ENV_VAR" ]]; then
if [[ ! -z "$PRIVATE_KEY_PASS" ]]; then
# write private key to disk and then import it keychain
echo -n -e ${PRIVATE_KEY_ENV_VAR} | base64 -D > ${PRIVATE_KEY_FILE_NAME}
# we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error
# will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below.
local out=$(security import ${PRIVATE_KEY_FILE_NAME} -k cloudflared_build_keychain -P "$PRIVATE_KEY_PASS" -T /usr/bin/pkgbuild -A -P "${PRIVATE_KEY_PASS}" 2>&1) || true
local exitcode=$?
rm -rf ${PRIVATE_KEY_FILE_NAME}
if [ -n "$out" ]; then
if [ $exitcode -eq 0 ]; then
echo "$out"
else
if [ "$out" != "${SEC_DUP_MSG}" ]; then
echo "$out" >&2
exit $exitcode
fi
fi
fi
fi
fi
}
# Create temp keychain only for this build
create_cloudflared_build_keychain "${CFD_CODE_SIGN_PASS}"
# Add Apple Root Developer certificate to the key chain
import_certificate "Apple Developer CA" "${APPLE_DEV_CA_CERT}" "${APPLE_CA_CERT}"
# Add code signing private key to the key chain
import_private_keys "Developer ID Application" "${CFD_CODE_SIGN_KEY}" "${CODE_SIGN_PRIV}" "${CFD_CODE_SIGN_PASS}"
# Add code signing certificate to the key chain
import_certificate "Developer ID Application" "${CFD_CODE_SIGN_CERT}" "${CODE_SIGN_CERT}"
# Add package signing private key to the key chain
import_private_keys "Developer ID Installer" "${CFD_INSTALLER_KEY}" "${INSTALLER_PRIV}" "${CFD_INSTALLER_PASS}"
# Add package signing certificate to the key chain
import_certificate "Developer ID Installer" "${CFD_INSTALLER_CERT}" "${INSTALLER_CERT}"
# get the code signing certificate name
if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then
CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
else
if [[ -n "$(security find-certificate -c "Developer ID Application" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
else
CODE_SIGN_NAME=""
fi
fi
# get the package signing certificate name
if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then
PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
else
if [[ -n "$(security find-certificate -c "Developer ID Installer" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
else
PKG_SIGN_NAME=""
fi
fi
# cleanup the build directory because the previous execution might have failed without cleaning up.
rm -rf "${TARGET_DIRECTORY}"
export TARGET_OS="darwin"
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
# This allows apple tools to use the certificates in the keychain without requiring password input.
# This command always needs to run after the certificates have been loaded into the keychain
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
security set-key-partition-list -S apple-tool:,apple: -s -k "${CFD_CODE_SIGN_PASS}" cloudflared_build_keychain
fi
# sign the cloudflared binary
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
codesign --keychain $HOME/Library/Keychains/cloudflared_build_keychain.keychain-db -s "${CODE_SIGN_NAME}" -fv --options runtime --timestamp ${BINARY_NAME}
# notarize the binary
# TODO: TUN-5789
fi
ARCH_TARGET_DIRECTORY="${TARGET_DIRECTORY}/${TARGET_ARCH}-build"
# creating build directory
rm -rf $ARCH_TARGET_DIRECTORY
mkdir -p "${ARCH_TARGET_DIRECTORY}"
mkdir -p "${ARCH_TARGET_DIRECTORY}/contents"
cp -r ".mac_resources/scripts" "${ARCH_TARGET_DIRECTORY}/scripts"
# copy cloudflared into the build directory
cp ${BINARY_NAME} "${ARCH_TARGET_DIRECTORY}/contents/${PRODUCT}"
# compress cloudflared into a tar and gzipped file
tar czf "$FILENAME" "${BINARY_NAME}"
# build the installer package
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${ARCH_TARGET_DIRECTORY}/scripts \
--root ${ARCH_TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
--keychain cloudflared_build_keychain \
--sign "${PKG_SIGN_NAME}" \
${PKGNAME}
# notarize the package
# TODO: TUN-5789
else
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${ARCH_TARGET_DIRECTORY}/scripts \
--root ${ARCH_TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PKGNAME}
fi
# cleanup build directory because this script is not ran within containers,
# which might lead to future issues in subsequent runs.
rm -rf "${TARGET_DIRECTORY}"
# cleanup the keychain
security default-keychain -d user -s login.keychain-db
security list-keychains -d user -s login.keychain-db
security delete-keychain cloudflared_build_keychain

10
.teamcity/mac/install-cloudflare-go.sh vendored Executable file
View File

@ -0,0 +1,10 @@
rm -rf /tmp/go
export GOCACHE=/tmp/gocache
rm -rf $GOCACHE
./.teamcity/install-cloudflare-go.sh
export PATH="/tmp/go/bin:$PATH"
go version
which go
go env

View File

@ -3,15 +3,17 @@ echo $VERSION
export TARGET_OS=windows export TARGET_OS=windows
# This controls the directory the built artifacts go into # This controls the directory the built artifacts go into
export ARTIFACT_DIR=built_artifacts/ export BUILT_ARTIFACT_DIR=built_artifacts/
mkdir -p $ARTIFACT_DIR export FINAL_ARTIFACT_DIR=artifacts/
mkdir -p $BUILT_ARTIFACT_DIR
mkdir -p $FINAL_ARTIFACT_DIR
windowsArchs=("amd64" "386") windowsArchs=("amd64" "386")
for arch in ${windowsArchs[@]}; do for arch in ${windowsArchs[@]}; do
export TARGET_ARCH=$arch export TARGET_ARCH=$arch
# Copy exe into final directory # Copy exe into final directory
cp ./artifacts/cloudflared-windows-$arch.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe cp $BUILT_ARTIFACT_DIR/cloudflared-windows-$arch.exe ./cloudflared.exe
cp ./artifacts/cloudflared-windows-$arch.exe ./cloudflared.exe
make cloudflared-msi make cloudflared-msi
# Copy msi into final directory # Copy msi into final directory
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi mv cloudflared-$VERSION-$arch.msi $FINAL_ARTIFACT_DIR/cloudflared-windows-$arch.msi
cp $BUILT_ARTIFACT_DIR/cloudflared-windows-$arch.exe $FINAL_ARTIFACT_DIR/cloudflared-windows-$arch.exe
done done

View File

@ -1,26 +0,0 @@
#!/bin/bash
set -euo pipefail
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
echo "Skipping public release for an untagged commit."
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
exit 0
fi
if [[ "${HOMEBREW_GITHUB_API_TOKEN:-}" == "" ]] ; then
echo "Missing GITHUB_API_TOKEN"
exit 1
fi
# "install" Homebrew
git clone https://github.com/Homebrew/brew tmp/homebrew
eval "$(tmp/homebrew/bin/brew shellenv)"
brew update --force --quiet
chmod -R go-w "$(brew --prefix)/share/zsh"
git config --global user.name "cloudflare-warp-bot"
git config --global user.email "warp-bot@cloudflare.com"
# bump formula pr
brew bump-formula-pr cloudflared --version="$VERSION" --no-browse --no-audit

View File

@ -1,66 +0,0 @@
#!/bin/bash
set -euo pipefail
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
echo "Skipping public release for an untagged commit."
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
exit 0
fi
if [[ ! -f "$FILENAME" ]] ; then
echo "Missing $FILENAME"
exit 1
fi
if [[ "${GITHUB_PRIVATE_KEY_B64:-}" == "" ]] ; then
echo "Missing GITHUB_PRIVATE_KEY_B64"
exit 1
fi
# upload to s3 bucket for use by Homebrew formula
s3cmd \
--acl-public --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
s3cmd \
--acl-public --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
mkdir -p tmp
ssh-keyscan -t rsa github.com > tmp/github.txt
echo "$GITHUB_PRIVATE_KEY_B64" | base64 --decode > tmp/private.key
chmod 0400 tmp/private.key
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
# clone Homebrew repo into tmp/homebrew-cloudflare
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
cd tmp/homebrew-cloudflare
git checkout -f master
git reset --hard origin/master
# modify cloudflared.rb
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
tee cloudflared.rb <<EOF
class Cloudflared < Formula
desc 'Cloudflare Tunnel'
homepage 'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps'
url '$URL'
sha256 '$SHA256'
version '$VERSION'
def install
bin.install 'cloudflared'
end
end
EOF
# push cloudflared.rb
git add cloudflared.rb
git diff
git config user.name "cloudflare-warp-bot"
git config user.email "warp-bot@cloudflare.com"
git commit -m "Release Cloudflare Tunnel $VERSION"
git push -v origin master

28
.teamcity/windows/builds.ps1 vendored Normal file
View File

@ -0,0 +1,28 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# Relative path to working directory
$CloudflaredDirectory = "go\src\github.com\cloudflare\cloudflared"
cd $CloudflaredDirectory
Write-Output "Building for amd64"
$env:TARGET_OS = "windows"
$env:CGO_ENABLED = 1
$env:TARGET_ARCH = "amd64"
$env:Path = "$Env:Temp\go\bin;$($env:Path)"
go env
go version
& make cloudflared
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for amd64" }
copy .\cloudflared.exe .\cloudflared-windows-amd64.exe
Write-Output "Building for 386"
$env:CGO_ENABLED = 0
$env:TARGET_ARCH = "386"
make cloudflared
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared for 386" }
copy .\cloudflared.exe .\cloudflared-windows-386.exe

47
.teamcity/windows/component-test.ps1 vendored Normal file
View File

@ -0,0 +1,47 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$WorkingDirectory = Get-Location
$CloudflaredDirectory = "$WorkingDirectory\go\src\github.com\cloudflare\cloudflared"
go env
go version
$env:TARGET_OS = "windows"
$env:CGO_ENABLED = 1
$env:TARGET_ARCH = "amd64"
$env:Path = "$Env:Temp\go\bin;$($env:Path)"
python --version
python -m pip --version
cd $CloudflaredDirectory
go env
go version
Write-Output "Building cloudflared"
& make cloudflared
if ($LASTEXITCODE -ne 0) { throw "Failed to build cloudflared" }
echo $LASTEXITCODE
Write-Output "Running unit tests"
# Not testing with race detector because of https://github.com/golang/go/issues/61058
# We already test it on other platforms
& go test -failfast -mod=vendor ./...
if ($LASTEXITCODE -ne 0) { throw "Failed unit tests" }
Write-Output "Running component tests"
python -m pip --disable-pip-version-check install --upgrade -r component-tests/requirements.txt --use-pep517
python component-tests/setup.py --type create
python -m pytest component-tests -o log_cli=true --log-cli-level=INFO
if ($LASTEXITCODE -ne 0) {
python component-tests/setup.py --type cleanup
throw "Failed component tests"
}
python component-tests/setup.py --type cleanup

View File

@ -0,0 +1,16 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
Write-Output "Downloading cloudflare go..."
Set-Location "$Env:Temp"
git clone -q https://github.com/cloudflare/go
Write-Output "Building go..."
cd go/src
# https://github.com/cloudflare/go/tree/af19da5605ca11f85776ef7af3384a02a315a52b is version go1.22.5-devel-cf
git checkout -q af19da5605ca11f85776ef7af3384a02a315a52b
& ./make.bat
Write-Output "Installed"

20
.teamcity/windows/install-go-msi.ps1 vendored Normal file
View File

@ -0,0 +1,20 @@
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$GoMsiVersion = "go1.22.5.windows-amd64.msi"
Write-Output "Downloading go installer..."
Set-Location "$Env:Temp"
(New-Object System.Net.WebClient).DownloadFile(
"https://go.dev/dl/$GoMsiVersion",
"$Env:Temp\$GoMsiVersion"
)
Write-Output "Installing go..."
Install-Package "$Env:Temp\$GoMsiVersion" -Force
# Go installer updates global $PATH
go env
Write-Output "Installed"

View File

@ -1,3 +1,26 @@
## 2025.1.1
### New Features
- This release introduces the use of new Post Quantum curves and the ability to use Post Quantum curves when running tunnels with the QUIC protocol this applies to non-FIPS and FIPS builds.
## 2024.12.2
### New Features
- This release introduces the ability to collect troubleshooting information from one instance of cloudflared running on the local machine. The command can be executed as `cloudflared tunnel diag`.
## 2024.12.1
### Notices
- The use of the `--metrics` is still honoured meaning that if this flag is set the metrics server will try to bind it, however, this version includes a change that makes the metrics server bind to a port with a semi-deterministic approach. If the metrics flag is not present the server will bind to the first available port of the range 20241 to 20245. In case of all ports being unavailable then the fallback is to bind to a random port.
## 2024.10.0
### Bug Fixes
- We fixed a bug related to `--grace-period`. Tunnels that use QUIC as transport weren't abiding by this waiting period before forcefully closing the connections to the edge. From now on, both QUIC and HTTP2 tunnels will wait for either the grace period to end (defaults to 30 seconds) or until the last in-flight request is handled. Users that wish to maintain the previous behavior should set `--grace-period` to 0 if `--protocol` is set to `quic`. This will force `cloudflared` to shutdown as soon as either SIGTERM or SIGINT is received.
## 2024.2.1
### Notices
- Starting from this version, tunnel diagnostics will be enabled by default. This will allow the engineering team to remotely get diagnostics from cloudflared during debug activities. Users still have the capability to opt-out of this feature by defining `--management-diagnostics=false` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`).
## 2023.9.0
### Notices
- The `warp-routing` `enabled: boolean` flag is no longer supported in the configuration file. Warp Routing traffic (eg TCP, UDP, ICMP) traffic is proxied to cloudflared if routes to the target tunnel are configured. This change does not affect remotely managed tunnels, but for locally managed tunnels, users that might be relying on this feature flag to block traffic should instead guarantee that tunnel has no Private Routes configured for the tunnel.
## 2023.7.0 ## 2023.7.0
### New Features ### New Features
- You can now enable additional diagnostics over the management.argotunnel.com service for your active cloudflared connectors via a new runtime flag `--management-diagnostics` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`). This feature is provided as opt-in and requires the flag to enable. Endpoints such as /metrics provides your prometheus metrics endpoint another mechanism to be reached. Additionally /debug/pprof/(goroutine|heap) are also introduced to allow for remotely retrieving active pprof information from a running cloudflared connector. - You can now enable additional diagnostics over the management.argotunnel.com service for your active cloudflared connectors via a new runtime flag `--management-diagnostics` (or env `TUNNEL_MANAGEMENT_DIAGNOSTICS`). This feature is provided as opt-in and requires the flag to enable. Endpoints such as /metrics provides your prometheus metrics endpoint another mechanism to be reached. Additionally /debug/pprof/(goroutine|heap) are also introduced to allow for remotely retrieving active pprof information from a running cloudflared connector.

View File

@ -1,22 +1,28 @@
# use a builder image for building cloudflare # use a builder image for building cloudflare
ARG TARGET_GOOS ARG TARGET_GOOS
ARG TARGET_GOARCH ARG TARGET_GOARCH
FROM golang:1.19 as builder FROM golang:1.22.10 as builder
ENV GO111MODULE=on \ ENV GO111MODULE=on \
CGO_ENABLED=0 \ CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \ TARGET_GOOS=${TARGET_GOOS} \
TARGET_GOARCH=${TARGET_GOARCH} TARGET_GOARCH=${TARGET_GOARCH} \
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
# which changes how cloudflared binds the metrics server
CONTAINER_BUILD=1
WORKDIR /go/src/github.com/cloudflare/cloudflared/ WORKDIR /go/src/github.com/cloudflare/cloudflared/
# copy our sources into the builder image # copy our sources into the builder image
COPY . . COPY . .
RUN .teamcity/install-cloudflare-go.sh
# compile cloudflared # compile cloudflared
RUN make cloudflared RUN PATH="/tmp/go/bin:$PATH" make cloudflared
# use a distroless base image with glibc # use a distroless base image with glibc
FROM gcr.io/distroless/base-debian11:nonroot FROM gcr.io/distroless/base-debian12:nonroot
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared" LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"

View File

@ -1,18 +1,23 @@
# use a builder image for building cloudflare # use a builder image for building cloudflare
FROM golang:1.19 as builder FROM golang:1.22.10 as builder
ENV GO111MODULE=on \ ENV GO111MODULE=on \
CGO_ENABLED=0 CGO_ENABLED=0 \
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
# which changes how cloudflared binds the metrics server
CONTAINER_BUILD=1
WORKDIR /go/src/github.com/cloudflare/cloudflared/ WORKDIR /go/src/github.com/cloudflare/cloudflared/
# copy our sources into the builder image # copy our sources into the builder image
COPY . . COPY . .
RUN .teamcity/install-cloudflare-go.sh
# compile cloudflared # compile cloudflared
RUN GOOS=linux GOARCH=amd64 make cloudflared RUN GOOS=linux GOARCH=amd64 PATH="/tmp/go/bin:$PATH" make cloudflared
# use a distroless base image with glibc # use a distroless base image with glibc
FROM gcr.io/distroless/base-debian11:nonroot FROM gcr.io/distroless/base-debian12:nonroot
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared" LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"

View File

@ -1,18 +1,23 @@
# use a builder image for building cloudflare # use a builder image for building cloudflare
FROM golang:1.19 as builder FROM golang:1.22.10 as builder
ENV GO111MODULE=on \ ENV GO111MODULE=on \
CGO_ENABLED=0 CGO_ENABLED=0 \
# the CONTAINER_BUILD envvar is used set github.com/cloudflare/cloudflared/metrics.Runtime=virtual
# which changes how cloudflared binds the metrics server
CONTAINER_BUILD=1
WORKDIR /go/src/github.com/cloudflare/cloudflared/ WORKDIR /go/src/github.com/cloudflare/cloudflared/
# copy our sources into the builder image # copy our sources into the builder image
COPY . . COPY . .
RUN .teamcity/install-cloudflare-go.sh
# compile cloudflared # compile cloudflared
RUN GOOS=linux GOARCH=arm64 make cloudflared RUN GOOS=linux GOARCH=arm64 PATH="/tmp/go/bin:$PATH" make cloudflared
# use a distroless base image with glibc # use a distroless base image with glibc
FROM gcr.io/distroless/base-debian11:nonroot-arm64 FROM gcr.io/distroless/base-debian12:nonroot-arm64
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared" LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"

114
Makefile
View File

@ -1,3 +1,6 @@
# The targets cannot be run in parallel
.NOTPARALLEL:
VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*") VERSION := $(shell git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-) MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup. #MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
@ -21,12 +24,16 @@ else
DEB_PACKAGE_NAME := $(BINARY_NAME) DEB_PACKAGE_NAME := $(BINARY_NAME)
endif endif
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC') DATE := $(shell date -u -r RELEASE_NOTES '+%Y-%m-%d-%H%M UTC')
VERSION_FLAGS := -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)" VERSION_FLAGS := -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"
ifdef PACKAGE_MANAGER ifdef PACKAGE_MANAGER
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)" VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)"
endif endif
ifdef CONTAINER_BUILD
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/metrics.Runtime=virtual"
endif
LINK_FLAGS := LINK_FLAGS :=
ifeq ($(FIPS), true) ifeq ($(FIPS), true)
LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS) LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS)
@ -49,6 +56,8 @@ PACKAGE_DIR := $(CURDIR)/packaging
PREFIX := /usr PREFIX := /usr
INSTALL_BINDIR := $(PREFIX)/bin/ INSTALL_BINDIR := $(PREFIX)/bin/
INSTALL_MANDIR := $(PREFIX)/share/man/man1/ INSTALL_MANDIR := $(PREFIX)/share/man/man1/
CF_GO_PATH := /tmp/go
PATH := $(CF_GO_PATH)/bin:$(PATH)
LOCAL_ARCH ?= $(shell uname -m) LOCAL_ARCH ?= $(shell uname -m)
ifneq ($(GOARCH),) ifneq ($(GOARCH),)
@ -124,11 +133,9 @@ clean:
cloudflared: cloudflared:
ifeq ($(FIPS), true) ifeq ($(FIPS), true)
$(info Building cloudflared with go-fips) $(info Building cloudflared with go-fips)
cp -f fips/fips.go.linux-amd64 cmd/cloudflared/fips.go
endif endif
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -v -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) $(ARM_COMMAND) go build -mod=vendor $(GO_BUILD_TAGS) $(LDFLAGS) $(IMPORT_PATH)/cmd/cloudflared
ifeq ($(FIPS), true) ifeq ($(FIPS), true)
rm -f cmd/cloudflared/fips.go
./check-fips.sh cloudflared ./check-fips.sh cloudflared
endif endif
@ -160,14 +167,31 @@ cover:
# Generate the HTML report that can be viewed from the browser in CI. # Generate the HTML report that can be viewed from the browser in CI.
$Q go tool cover -html ".cover/c.out" -o .cover/all.html $Q go tool cover -html ".cover/c.out" -o .cover/all.html
.PHONY: test-ssh-server .PHONY: fuzz
test-ssh-server: fuzz:
docker-compose -f ssh_server_tests/docker-compose.yml up @go test -fuzz=FuzzIPDecoder -fuzztime=600s ./packet
@go test -fuzz=FuzzICMPDecoder -fuzztime=600s ./packet
@go test -fuzz=FuzzSessionWrite -fuzztime=600s ./quic/v3
@go test -fuzz=FuzzSessionServe -fuzztime=600s ./quic/v3
@go test -fuzz=FuzzRegistrationDatagram -fuzztime=600s ./quic/v3
@go test -fuzz=FuzzPayloadDatagram -fuzztime=600s ./quic/v3
@go test -fuzz=FuzzRegistrationResponseDatagram -fuzztime=600s ./quic/v3
@go test -fuzz=FuzzNewIdentity -fuzztime=600s ./tracing
@go test -fuzz=FuzzNewAccessValidator -fuzztime=600s ./validation
.PHONY: install-go
install-go:
rm -rf ${CF_GO_PATH}
./.teamcity/install-cloudflare-go.sh
.PHONY: cleanup-go
cleanup-go:
rm -rf ${CF_GO_PATH}
cloudflared.1: cloudflared_man_template cloudflared.1: cloudflared_man_template
sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' cloudflared_man_template > cloudflared.1 sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' cloudflared_man_template > cloudflared.1
install: cloudflared cloudflared.1 install: install-go cloudflared cloudflared.1 cleanup-go
mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR) mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR)
install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared
install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1 install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1
@ -204,64 +228,46 @@ cloudflared-pkg: cloudflared cloudflared.1
cloudflared-msi: cloudflared-msi:
wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs
.PHONY: cloudflared-darwin-amd64.tgz .PHONY: github-release-dryrun
cloudflared-darwin-amd64.tgz: cloudflared github-release-dryrun:
tar czf cloudflared-darwin-amd64.tgz cloudflared python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION) --dry-run
rm cloudflared
.PHONY: homebrew-upload
homebrew-upload: cloudflared-darwin-amd64.tgz
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $$^ $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz
aws s3 --endpoint-url $(S3_ENDPOINT) cp --acl public-read $(S3_URI)/cloudflared-$$(VERSION)-$1.tgz $(S3_URI)/cloudflared-stable-$1.tgz
.PHONY: homebrew-release
homebrew-release: homebrew-upload
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
.PHONY: github-release .PHONY: github-release
github-release: cloudflared github-release:
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
.PHONY: github-release-built-pkgs
github-release-built-pkgs:
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION) python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
.PHONY: release-pkgs-linux
release-pkgs-linux:
python3 ./release_pkgs.py
.PHONY: github-message
github-message:
python3 github_message.py --release-version $(VERSION) python3 github_message.py --release-version $(VERSION)
.PHONY: github-mac-upload .PHONY: macos-release
github-mac-upload: macos-release:
python3 github_release.py --path artifacts/cloudflared-darwin-amd64.tgz --release-version $(VERSION) --name cloudflared-darwin-amd64.tgz python3 github_release.py --path $(PWD)/artifacts/ --release-version $(VERSION)
python3 github_release.py --path artifacts/cloudflared-amd64.pkg --release-version $(VERSION) --name cloudflared-amd64.pkg
.PHONY: github-windows-upload .PHONY: r2-linux-release
github-windows-upload: r2-linux-release:
python3 github_release.py --path built_artifacts/cloudflared-windows-amd64.exe --release-version $(VERSION) --name cloudflared-windows-amd64.exe python3 ./release_pkgs.py
python3 github_release.py --path built_artifacts/cloudflared-windows-amd64.msi --release-version $(VERSION) --name cloudflared-windows-amd64.msi
python3 github_release.py --path built_artifacts/cloudflared-windows-386.exe --release-version $(VERSION) --name cloudflared-windows-386.exe
python3 github_release.py --path built_artifacts/cloudflared-windows-386.msi --release-version $(VERSION) --name cloudflared-windows-386.msi
.PHONY: tunnelrpc-deps .PHONY: capnp
tunnelrpc-deps: capnp:
which capnp # https://capnproto.org/install.html which capnp # https://capnproto.org/install.html
which capnpc-go # go install zombiezen.com/go/capnproto2/capnpc-go@latest which capnpc-go # go install zombiezen.com/go/capnproto2/capnpc-go@latest
capnp compile -ogo tunnelrpc/tunnelrpc.capnp capnp compile -ogo tunnelrpc/proto/tunnelrpc.capnp tunnelrpc/proto/quic_metadata_protocol.capnp
.PHONY: quic-deps
quic-deps:
which capnp
which capnpc-go
capnp compile -ogo quic/schema/quic_metadata_protocol.capnp
.PHONY: vet .PHONY: vet
vet: vet:
go vet -v -mod=vendor github.com/cloudflare/cloudflared/... go vet -mod=vendor github.com/cloudflare/cloudflared/...
.PHONY: fmt .PHONY: fmt
fmt: fmt:
goimports -l -w -local github.com/cloudflare/cloudflared $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) @goimports -l -w -local github.com/cloudflare/cloudflared $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc/proto)
@go fmt $$(go list -mod=vendor -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc/proto)
.PHONY: fmt-check
fmt-check:
@./fmt-check.sh
.PHONY: lint
lint:
@golangci-lint run
.PHONY: mocks
mocks:
go generate mocks/mockgen.go

View File

@ -31,7 +31,7 @@ Downloads are available as standalone binaries, a Docker image, and Debian, RPM,
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux) * Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#linux)
* A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared) * A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared)
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows) * You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
* Build from source with the [instructions here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#build-from-source) * To build from source, first you need to download the go toolchain by running `./.teamcity/install-cloudflare-go.sh` and follow the output. Then you can run `make cloudflared`
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
@ -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. 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/connections/connect-apps/create-tunnel) * Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/)
* Route traffic to that Tunnel: * Route traffic to that Tunnel:
* Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns) * Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns)
* Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb) * Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb)
@ -49,13 +49,34 @@ Once installed, you can authenticate `cloudflared` into your Cloudflare account
## TryCloudflare ## TryCloudflare
Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare). Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/).
## Deprecated versions ## Deprecated versions
Cloudflare currently supports versions of `cloudflared` 2020.5.1 and later. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared). Cloudflare currently supports versions of cloudflared that are **within one year** of the most recent release. Breaking changes unrelated to feature availability may be introduced that will impact versions released more than one year ago. You can read more about upgrading cloudflared in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/#updating-cloudflared).
| Version(s) | Deprecation status | For example, as of January 2023 Cloudflare will support cloudflared version 2023.1.1 to cloudflared 2022.1.1.
|---|---|
| 2020.5.1 and later | Supported | ## Development
| Versions prior to 2020.5.1 | No longer supported |
### Requirements
- [GNU Make](https://www.gnu.org/software/make/)
- [capnp](https://capnproto.org/install.html)
- [cloudflare go toolchain](https://github.com/cloudflare/go)
- 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)
- [golangci-lint](https://github.com/golangci/golangci-lint)
- [gomocks](https://pkg.go.dev/go.uber.org/mock)
### Build
To build cloudflared locally run `make cloudflared`
### Test
To locally run the tests run `make test`
### Linting
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`

View File

@ -1,3 +1,302 @@
2025.4.2
- 2025-04-30 chore: Do not use gitlab merge request pipelines
- 2025-04-30 DEVTOOLS-16383: Create GitlabCI pipeline to release Mac builds
- 2025-04-24 TUN-9255: Improve flush on write conditions in http2 tunnel type to match what is done on the edge
- 2025-04-10 SDLC-3727 - Adding FIPS status to backstage
2025.4.0
- 2025-04-02 Fix broken links in `cmd/cloudflared/*.go` related to running tunnel as a service
- 2025-04-02 chore: remove repetitive words
- 2025-04-01 Fix messages to point to one.dash.cloudflare.com
- 2025-04-01 feat: emit explicit errors for the `service` command on unsupported OSes
- 2025-04-01 Use RELEASE_NOTES date instead of build date
- 2025-04-01 chore: Update tunnel configuration link in the readme
- 2025-04-01 fix: expand home directory for credentials file
- 2025-04-01 fix: Use path and filepath operation appropriately
- 2025-04-01 feat: Adds a new command line for tunnel run for token file
- 2025-04-01 chore: fix linter rules
- 2025-03-17 TUN-9101: Don't ignore errors on `cloudflared access ssh`
- 2025-03-06 TUN-9089: Pin go import to v0.30.0, v0.31.0 requires go 1.23
2025.2.1
- 2025-02-26 TUN-9016: update base-debian to v12
- 2025-02-25 TUN-8960: Connect to FED API GW based on the OriginCert's endpoint
- 2025-02-25 TUN-9007: modify logic to resolve region when the tunnel token has an endpoint field
- 2025-02-13 SDLC-3762: Remove backstage.io/source-location from catalog-info.yaml
- 2025-02-06 TUN-8914: Create a flags module to group all cloudflared cli flags
2025.2.0
- 2025-02-03 TUN-8914: Add a new configuration to locally override the max-active-flows
- 2025-02-03 Bump x/crypto to 0.31.0
2025.1.1
- 2025-01-30 TUN-8858: update go to 1.22.10 and include quic-go FIPS changes
- 2025-01-30 TUN-8855: fix lint issues
- 2025-01-30 TUN-8855: Update PQ curve preferences
- 2025-01-30 TUN-8857: remove restriction for using FIPS and PQ
- 2025-01-30 TUN-8894: report FIPS+PQ error to Sentry when dialling to the edge
- 2025-01-22 TUN-8904: Rename Connect Response Flow Rate Limited metadata
- 2025-01-21 AUTH-6633 Fix cloudflared access login + warp as auth
- 2025-01-20 TUN-8861: Add session limiter to UDP session manager
- 2025-01-20 TUN-8861: Rename Session Limiter to Flow Limiter
- 2025-01-17 TUN-8900: Add import of Apple Developer Certificate Authority to macOS Pipeline
- 2025-01-17 TUN-8871: Accept login flag to authenticate with Fedramp environment
- 2025-01-16 TUN-8866: Add linter to cloudflared repository
- 2025-01-14 TUN-8861: Add session limiter to TCP session manager
- 2025-01-13 TUN-8861: Add configuration for active sessions limiter
- 2025-01-09 TUN-8848: Don't treat connection shutdown as an error condition when RPC server is done
2025.1.0
- 2025-01-06 TUN-8842: Add Ubuntu Noble and 'any' debian distributions to release script
- 2025-01-06 TUN-8807: Add support_datagram_v3 to remote feature rollout
- 2024-12-20 TUN-8829: add CONTAINER_BUILD to dockerfiles
2024.12.2
- 2024-12-19 TUN-8822: Prevent concurrent usage of ICMPDecoder
- 2024-12-18 TUN-8818: update changes document to reflect newly added diag subcommand
- 2024-12-17 TUN-8817: Increase close session channel by one since there are two writers
- 2024-12-13 TUN-8797: update CHANGES.md with note about semi-deterministic approach used to bind metrics server
- 2024-12-13 TUN-8724: Add CLI command for diagnostic procedure
- 2024-12-11 TUN-8786: calculate cli flags once for the diagnostic procedure
- 2024-12-11 TUN-8792: Make diag/system endpoint always return a JSON
- 2024-12-10 TUN-8783: fix log collectors for the diagnostic procedure
- 2024-12-10 TUN-8785: include the icmp sources in the diag's tunnel state
- 2024-12-10 TUN-8784: Set JSON encoder options to print formatted JSON when writing diag files
2024.12.1
- 2024-12-10 TUN-8795: update createrepo to createrepo_c to fix the release_pkgs.py script
2024.12.0
- 2024-12-09 TUN-8640: Add ICMP support for datagram V3
- 2024-12-09 TUN-8789: make python package installation consistent
- 2024-12-06 TUN-8781: Add Trixie, drop Buster. Default to Bookworm
- 2024-12-05 TUN-8775: Make sure the session Close can only be called once
- 2024-12-04 TUN-8725: implement diagnostic procedure
- 2024-12-04 TUN-8767: include raw output from network collector in diagnostic zipfile
- 2024-12-04 TUN-8770: add cli configuration and tunnel configuration to diagnostic zipfile
- 2024-12-04 TUN-8768: add job report to diagnostic zipfile
- 2024-12-03 TUN-8726: implement compression routine to be used in diagnostic procedure
- 2024-12-03 TUN-8732: implement port selection algorithm
- 2024-12-03 TUN-8762: fix argument order when invoking tracert and modify network info output parsing.
- 2024-12-03 TUN-8769: fix k8s log collector arguments
- 2024-12-03 TUN-8727: extend client to include function to get cli configuration and tunnel configuration
- 2024-11-29 TUN-8729: implement network collection for diagnostic procedure
- 2024-11-29 TUN-8727: implement metrics, runtime, system, and tunnelstate in diagnostic http client
- 2024-11-27 TUN-8733: add log collection for docker
- 2024-11-27 TUN-8734: add log collection for kubernetes
- 2024-11-27 TUN-8640: Refactor ICMPRouter to support new ICMPResponders
- 2024-11-26 TUN-8735: add managed/local log collection
- 2024-11-25 TUN-8728: implement diag/tunnel endpoint
- 2024-11-25 TUN-8730: implement diag/configuration
- 2024-11-22 TUN-8737: update metrics server port selection
- 2024-11-22 TUN-8731: Implement diag/system endpoint
- 2024-11-21 TUN-8748: Migrated datagram V3 flows to use migrated context
2024.11.1
- 2024-11-18 Add cloudflared tunnel ready command
- 2024-11-14 Make metrics a requirement for tunnel ready command
- 2024-11-12 TUN-8701: Simplify flow registration logs for datagram v3
- 2024-11-11 add: new go-fuzz targets
- 2024-11-07 TUN-8701: Add metrics and adjust logs for datagram v3
- 2024-11-06 TUN-8709: Add session migration for datagram v3
- 2024-11-04 Fixed 404 in README.md to TryCloudflare
- 2024-09-24 Update semgrep.yml
2024.11.0
- 2024-11-05 VULN-66059: remove ssh server tests
- 2024-11-04 TUN-8700: Add datagram v3 muxer
- 2024-11-04 TUN-8646: Allow experimental feature support for datagram v3
- 2024-11-04 TUN-8641: Expose methods to simplify V3 Datagram parsing on the edge
- 2024-10-31 TUN-8708: Bump python min version to 3.10
- 2024-10-31 TUN-8667: Add datagram v3 session manager
- 2024-10-25 TUN-8692: remove dashes from session id
- 2024-10-24 TUN-8694: Rework release script
- 2024-10-24 TUN-8661: Refactor connection methods to support future different datagram muxing methods
- 2024-07-22 TUN-8553: Bump go to 1.22.5 and go-boring 1.22.5-1
2024.10.1
- 2024-10-23 TUN-8694: Fix github release script
- 2024-10-21 Revert "TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport"
- 2024-10-18 TUN-8688: Correct UDP bind for IPv6 edge connectivity on macOS
- 2024-10-17 TUN-8685: Bump coredns dependency
- 2024-10-16 TUN-8638: Add datagram v3 serializers and deserializers
- 2024-10-15 chore: Remove h2mux code
- 2024-10-11 TUN-8631: Abort release on version mismatch
2024.10.0
- 2024-10-01 TUN-8646: Add datagram v3 support feature flag
- 2024-09-30 TUN-8621: Fix cloudflared version in change notes to account for release date
- 2024-09-19 Adding semgrep yaml file
- 2024-09-12 TUN-8632: Delay checking auto-update by the provided frequency
- 2024-09-11 TUN-8630: Check checksum of downloaded binary to compare to current for auto-updating
- 2024-09-09 TUN-8629: Cloudflared update on Windows requires running it twice to update
- 2024-09-06 PPIP-2310: Update quick tunnel disclaimer
- 2024-08-30 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering
- 2024-08-09 TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport
- 2024-06-26 TUN-8484: Print response when QuickTunnel can't be unmarshalled
2024.9.1
- 2024-09-10 Revert Release 2024.9.0
2024.9.0
- 2024-09-10 TUN-8621: Fix cloudflared version in change notes.
- 2024-09-06 PPIP-2310: Update quick tunnel disclaimer
- 2024-08-30 TUN-8621: Prevent QUIC connection from closing before grace period after unregistering
- 2024-08-09 TUN-8592: Use metadata from the edge to determine if request body is empty for QUIC transport
- 2024-06-26 TUN-8484: Print response when QuickTunnel can't be unmarshalled
2024.8.3
- 2024-08-15 TUN-8591 login command without extra text
- 2024-03-25 remove code that will not be executed
- 2024-03-25 remove code that will not be executed
2024.8.2
- 2024-08-05 TUN-8583: change final directory of artifacts
- 2024-08-05 TUN-8585: Avoid creating GH client when dry-run is true
2024.7.3
- 2024-07-31 TUN-8546: Fix final artifacts paths
2024.7.2
- 2024-07-17 TUN-8546: rework MacOS build script
2024.7.1
- 2024-07-16 TUN-8543: use -p flag to create intermediate directories
2024.7.0
- 2024-07-05 TUN-8520: add macos arm64 build
- 2024-07-05 TUN-8523: refactor makefile and cfsetup
- 2024-07-02 TUN-8504: Use pre-installed python version instead of downloading it on Windows builds
- 2024-06-26 TUN-8489: Add default noop logger for capnprpc
- 2024-06-25 TUN-8487: Add user-agent for quick-tunnel requests
- 2023-12-12 TUN-8057: cloudflared uses new PQ curve ID
2024.6.1
- 2024-06-12 TUN-8461: Don't log Failed to send session payload if the error is EOF
- 2024-06-07 TUN-8456: Update quic-go to 0.45 and collect mtu and congestion control metrics
- 2024-06-06 TUN-8452: Add flag to control QUIC stream-level flow control limit
- 2024-06-06 TUN-8451: Log QUIC flow control frames and transport parameters received
- 2024-06-05 TUN-8449: Add flag to control QUIC connection-level flow control limit and increase default to 30MB
2024.6.0
- 2024-05-30 TUN-8441: Correct UDP total sessions metric to a counter and add new ICMP metrics
- 2024-05-28 TUN-8422: Add metrics for capnp method calls
- 2024-05-24 TUN-8424: Refactor capnp registration server
- 2024-05-23 TUN-8427: Fix BackoffHandler's internally shared clock structure
- 2024-05-21 TUN-8425: Remove ICMP binding for quick tunnels
- 2024-05-20 TUN-8423: Deprecate older legacy tunnel capnp interfaces
- 2024-05-15 TUN-8419: Add capnp safe transport
- 2024-05-13 TUN-8415: Refactor capnp rpc into a single module
2024.5.0
- 2024-05-07 TUN-8407: Upgrade go to version 1.22.2
2024.4.1
- 2024-04-22 TUN-8380: Add sleep before requesting quick tunnel as temporary fix for component tests
- 2024-04-19 TUN-8374: Close UDP socket if registration fails
- 2024-04-18 TUN-8371: Bump quic-go to v0.42.0
- 2024-04-03 TUN-8333: Bump go-jose dependency to v4
- 2024-04-02 TUN-8331: Add unit testing for AccessJWTValidator middleware
2024.4.0
- 2024-04-02 feat: provide short version (#1206)
- 2024-04-02 Format code
- 2024-01-18 feat: auto tls sni
- 2023-12-24 fix checkInPingGroup bugs
- 2023-12-15 Add environment variables for TCP tunnel hostname / destination / URL.
2024.3.0
- 2024-03-14 TUN-8281: Run cloudflared query list tunnels/routes endpoint in a paginated way
- 2024-03-13 TUN-8297: Improve write timeout logging on safe_stream.go
- 2024-03-07 TUN-8290: Remove `|| true` from postrm.sh
- 2024-03-05 TUN-8275: Skip write timeout log on "no network activity"
- 2024-01-23 Update postrm.sh to fix incomplete uninstall
- 2024-01-05 fix typo in errcheck for response parsing logic in CreateTunnel routine
- 2023-12-23 Update linux_service.go
- 2023-12-07 ci: bump actions/checkout to v4
- 2023-12-07 ci/check: bump actions/setup-go to v5
- 2023-04-28 check.yaml: bump actions/setup-go to v4
2024.2.1
- 2024-02-20 TUN-8242: Update Changes.md file with new remote diagnostics behaviour
- 2024-02-19 TUN-8238: Fix type mismatch introduced by fast-forward
- 2024-02-16 TUN-8243: Collect metrics on the number of QUIC frames sent/received
- 2024-02-15 TUN-8238: Refactor proxy logging
- 2024-02-14 TUN-8242: Enable remote diagnostics by default
- 2024-02-12 TUN-8236: Add write timeout to quic and tcp connections
- 2024-02-09 TUN-8224: Fix safety of TCP stream logging, separate connect and ack log messages
2024.2.0
- 2024-02-07 TUN-8224: Count and collect metrics on stream connect successes/errors
2024.1.5
- 2024-01-22 TUN-8176: Support ARM platforms that don't have an FPU or have it enabled in kernel
- 2024-01-15 TUN-8158: Bring back commit e6537418859afcac29e56a39daa08bcabc09e048 and fixes infinite loop on linux when the socket is closed
2024.1.4
- 2024-01-19 Revert "TUN-8158: Add logging to confirm when ICMP reply is returned to the edge"
2024.1.3
- 2024-01-15 TUN-8161: Fix broken ARM build for armv6
- 2024-01-15 TUN-8158: Add logging to confirm when ICMP reply is returned to the edge
2024.1.2
- 2024-01-11 TUN-8147: Disable ECN usage due to bugs in detecting if supported
- 2024-01-11 TUN-8146: Fix export path for install-go command
- 2024-01-11 TUN-8146: Fix Makefile targets should not be run in parallel and install-go script was missing shebang
- 2024-01-10 TUN-8140: Remove homebrew scripts
2024.1.1
- 2024-01-10 TUN-8134: Revert installed prefix to /usr
- 2024-01-09 TUN-8130: Fix path to install go for mac build
- 2024-01-09 TUN-8129: Use the same build command between branch and release builds
- 2024-01-09 TUN-8130: Install go tool chain in /tmp on build agents
- 2024-01-09 TUN-8134: Install cloudflare go as part of make install
- 2024-01-08 TUN-8118: Disable FIPS module to build with go-boring without CGO_ENABLED
2024.1.0
- 2024-01-01 TUN-7934: Update quic-go to a version that queues datagrams for better throughput and drops large datagram
- 2023-12-20 TUN-8072: Need to set GOCACHE in mac go installation script
- 2023-12-17 TUN-8072: Add script to download cloudflare go for Mac build agents
- 2023-12-15 Fix nil pointer dereference segfault when passing "null" config json to cloudflared tunnel ingress validate (#1070)
- 2023-12-15 configuration.go: fix developerPortal link (#960)
- 2023-12-14 tunnelrpc/pogs: fix dropped test errors (#1106)
- 2023-12-14 cmd/cloudflared/updater: fix dropped error (#1055)
- 2023-12-14 use os.Executable to discover the path to cloudflared (#1040)
- 2023-12-14 Remove extraneous `period` from Path Environment Variable (#1009)
- 2023-12-14 Use CLI context when running tunnel (#597)
- 2023-12-14 TUN-8066: Define scripts to build on Windows agents
- 2023-12-11 TUN-8052: Update go to 1.21.5
- 2023-12-07 TUN-7970: Default to enable post quantum encryption for quic transport
- 2023-12-04 TUN-8006: Update quic-go to latest upstream
- 2023-11-15 VULN-44842 Add a flag that allows users to not send the Access JWT to stdout
- 2023-11-13 TUN-7965: Remove legacy incident status page check
- 2023-11-13 AUTH-5682 Org token flow in Access logins should pass CF_AppSession cookie
2023.10.0
- 2023-10-06 TUN-7864: Document cloudflared versions support
- 2023-10-03 CUSTESC-33731: Make rule match test report rule in 0-index base
- 2023-09-22 TUN-7824: Fix usage of systemctl status to detect which services are installed
- 2023-09-20 TUN-7813: Improve tunnel delete command to use cascade delete
- 2023-09-20 TUN-7787: cloudflared only list ip routes targeted for cfd_tunnel
- 2023-09-15 TUN-7787: Refactor cloudflared to use new route endpoints based on route IDs
- 2023-09-08 TUN-7776: Remove warp-routing flag from cloudflared
- 2023-09-05 TUN-7756: Clarify that QUIC is mandatory to support ICMP proxying
2023.8.2
- 2023-08-25 TUN-7700: Implement feature selector to determine if connections will prefer post quantum cryptography
- 2023-08-22 TUN-7707: Use X25519Kyber768Draft00 curve when post-quantum feature is enabled
2023.8.1
- 2023-08-23 TUN-7718: Update R2 Token to no longer encode secret
2023.8.0
- 2023-07-26 TUN-7584: Bump go 1.20.6
2023.7.3
- 2023-07-25 TUN-7628: Correct Host parsing for Access
- 2023-07-24 TUN-7624: Fix flaky TestBackoffGracePeriod test in cloudflared
2023.7.2 2023.7.2
- 2023-07-19 TUN-7599: Onboard cloudflared to Software Dashboard - 2023-07-19 TUN-7599: Onboard cloudflared to Software Dashboard
- 2023-07-19 TUN-7587: Remove junos builds - 2023-07-19 TUN-7587: Remove junos builds

View File

@ -1,8 +1,9 @@
#!/bin/bash
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*") VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
echo $VERSION echo $VERSION
# This controls the directory the built artifacts go into # This controls the directory the built artifacts go into
export ARTIFACT_DIR=built_artifacts/ export ARTIFACT_DIR=artifacts/
mkdir -p $ARTIFACT_DIR mkdir -p $ARTIFACT_DIR
arch=("amd64") arch=("amd64")
@ -16,7 +17,7 @@ make cloudflared-deb
mv cloudflared-fips\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-fips-linux-$arch.deb mv cloudflared-fips\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-fips-linux-$arch.deb
# rpm packages invert the - and _ and use x86_64 instead of amd64. # rpm packages invert the - and _ and use x86_64 instead of amd64.
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g') RPMVERSION=$(echo $VERSION | sed -r 's/-/_/g')
RPMARCH="x86_64" RPMARCH="x86_64"
make cloudflared-rpm make cloudflared-rpm
mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm

View File

@ -1,11 +1,13 @@
#!/bin/bash
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*") VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
echo $VERSION echo $VERSION
# Avoid depending on C code since we don't need it. # Disable FIPS module in go-boring
export GOEXPERIMENT=noboringcrypto
export CGO_ENABLED=0 export CGO_ENABLED=0
# This controls the directory the built artifacts go into # This controls the directory the built artifacts go into
export ARTIFACT_DIR=built_artifacts/ export ARTIFACT_DIR=artifacts/
mkdir -p $ARTIFACT_DIR mkdir -p $ARTIFACT_DIR
linuxArchs=("386" "amd64" "arm" "armhf" "arm64") linuxArchs=("386" "amd64" "arm" "armhf" "arm64")
@ -14,6 +16,12 @@ for arch in ${linuxArchs[@]}; do
unset TARGET_ARM unset TARGET_ARM
export TARGET_ARCH=$arch export TARGET_ARCH=$arch
## Support for arm platforms without hardware FPU enabled
if [[ $arch == arm ]] ; then
export TARGET_ARCH=arm
export TARGET_ARM=5
fi
## Support for armhf builds ## Support for armhf builds
if [[ $arch == armhf ]] ; then if [[ $arch == armhf ]] ; then
export TARGET_ARCH=arm export TARGET_ARCH=arm

View File

@ -4,7 +4,6 @@ metadata:
name: cloudflared name: cloudflared
description: Client for Cloudflare Tunnels description: Client for Cloudflare Tunnels
annotations: annotations:
backstage.io/source-location: url:https://bitbucket.cfdata.org/projects/TUN/repos/cloudflared/browse
cloudflare.com/software-excellence-opt-in: "true" cloudflare.com/software-excellence-opt-in: "true"
cloudflare.com/jira-project-key: "TUN" cloudflare.com/jira-project-key: "TUN"
cloudflare.com/jira-project-component: "Cloudflare Tunnel" cloudflare.com/jira-project-component: "Cloudflare Tunnel"
@ -14,3 +13,5 @@ spec:
type: "service" type: "service"
lifecycle: "Active" lifecycle: "Active"
owner: "teams/tunnel-teams-routing" owner: "teams/tunnel-teams-routing"
cf:
FIPS: "required"

View File

@ -109,20 +109,34 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (
return r.client.Do(req) return r.client.Do(req)
} }
func parseResponse(reader io.Reader, data interface{}) error { func parseResponseEnvelope(reader io.Reader) (*response, error) {
// Schema for Tunnelstore responses in the v1 API. // Schema for Tunnelstore responses in the v1 API.
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc // Roughly, it's a wrapper around a particular result that adds failures/errors/etc
var result response var result response
// First, parse the wrapper and check the API call succeeded // First, parse the wrapper and check the API call succeeded
if err := json.NewDecoder(reader).Decode(&result); err != nil { if err := json.NewDecoder(reader).Decode(&result); err != nil {
return errors.Wrap(err, "failed to decode response") return nil, errors.Wrap(err, "failed to decode response")
} }
if err := result.checkErrors(); err != nil { if err := result.checkErrors(); err != nil {
return err return nil, err
} }
if !result.Success { if !result.Success {
return ErrAPINoSuccess return nil, ErrAPINoSuccess
} }
return &result, nil
}
func parseResponse(reader io.Reader, data interface{}) error {
result, err := parseResponseEnvelope(reader)
if err != nil {
return err
}
return parseResponseBody(result, data)
}
func parseResponseBody(result *response, data interface{}) error {
// At this point we know the API call succeeded, so, parse out the inner // At this point we know the API call succeeded, so, parse out the inner
// result into the datatype provided as a parameter. // result into the datatype provided as a parameter.
if err := json.Unmarshal(result.Result, &data); err != nil { if err := json.Unmarshal(result.Result, &data); err != nil {
@ -131,11 +145,58 @@ func parseResponse(reader io.Reader, data interface{}) error {
return nil return nil
} }
func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T, error) {
page := 0
var fullResponse []*T
for {
page += 1
envelope, parsedBody, err := fetchPage[T](requestFn, page)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("Error Parsing page %d", page))
}
fullResponse = append(fullResponse, parsedBody...)
if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
break
}
}
return fullResponse, nil
}
func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*response, []*T, error) {
pageResp, err := requestFn(page)
if err != nil {
return nil, nil, errors.Wrap(err, "REST request failed")
}
defer pageResp.Body.Close()
if pageResp.StatusCode == http.StatusOK {
envelope, err := parseResponseEnvelope(pageResp.Body)
if err != nil {
return nil, nil, err
}
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 { type response struct {
Success bool `json:"success,omitempty"` Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"` Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"` Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"` Result json.RawMessage `json:"result,omitempty"`
Pagination Pagination `json:"result_info,omitempty"`
}
type Pagination struct {
Count int `json:"count,omitempty"`
Page int `json:"page,omitempty"`
PerPage int `json:"per_page,omitempty"`
TotalCount int `json:"total_count,omitempty"`
} }
func (r *response) checkErrors() error { func (r *response) checkErrors() error {

View File

@ -9,7 +9,7 @@ type TunnelClient interface {
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
GetTunnelToken(tunnelID uuid.UUID) (string, error) GetTunnelToken(tunnelID uuid.UUID) (string, error)
GetManagementToken(tunnelID uuid.UUID) (string, error) GetManagementToken(tunnelID uuid.UUID) (string, error)
DeleteTunnel(tunnelID uuid.UUID) error DeleteTunnel(tunnelID uuid.UUID, cascade bool) error
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
@ -22,7 +22,7 @@ type HostnameClient interface {
type IPRouteClient interface { type IPRouteClient interface {
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
AddRoute(newRoute NewRoute) (Route, error) AddRoute(newRoute NewRoute) (Route, error)
DeleteRoute(params DeleteRouteParams) error DeleteRoute(id uuid.UUID) error
GetByIP(params GetRouteByIpParams) (DetailedRoute, error) GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
} }

View File

@ -75,10 +75,12 @@ type NewRoute struct {
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet). // MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
func (r NewRoute) MarshalJSON() ([]byte, error) { func (r NewRoute) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct { return json.Marshal(&struct {
Network string `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"` TunnelID uuid.UUID `json:"tunnel_id"`
Comment string `json:"comment"` Comment string `json:"comment"`
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"` VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
}{ }{
Network: r.Network.String(),
TunnelID: r.TunnelID, TunnelID: r.TunnelID,
Comment: r.Comment, Comment: r.Comment,
VNetID: r.VNetID, VNetID: r.VNetID,
@ -87,6 +89,7 @@ func (r NewRoute) MarshalJSON() ([]byte, error) {
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName. // DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
type DetailedRoute struct { type DetailedRoute struct {
ID uuid.UUID `json:"id"`
Network CIDR `json:"network"` Network CIDR `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"` TunnelID uuid.UUID `json:"tunnel_id"`
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network. // Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
@ -115,7 +118,8 @@ func (r DetailedRoute) TableString() string {
} }
return fmt.Sprintf( return fmt.Sprintf(
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t", "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
r.ID,
r.Network.String(), r.Network.String(),
vnetColumn, vnetColumn,
r.Comment, r.Comment,
@ -126,12 +130,6 @@ func (r DetailedRoute) TableString() string {
) )
} }
type DeleteRouteParams struct {
Network net.IPNet
// Optional field. If unset, backend will assume the default vnet for the account.
VNetID *uuid.UUID
}
type GetRouteByIpParams struct { type GetRouteByIpParams struct {
Ip net.IP Ip net.IP
// Optional field. If unset, backend will assume the default vnet for the account. // Optional field. If unset, backend will assume the default vnet for the account.
@ -139,26 +137,30 @@ type GetRouteByIpParams struct {
} }
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account. // ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
// Due to pagination on the server side it will call the endpoint multiple times if needed.
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) { func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
endpoint := r.baseEndpoints.accountRoutes fetchFn := func(page int) (*http.Response, error) {
endpoint.RawQuery = filter.Encode() endpoint := r.baseEndpoints.accountRoutes
resp, err := r.sendRequest("GET", endpoint, nil) filter.Page(page)
if err != nil { endpoint.RawQuery = filter.Encode()
return nil, errors.Wrap(err, "REST request failed") rsp, err := r.sendRequest("GET", endpoint, nil)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK { if err != nil {
return parseListDetailedRoutes(resp.Body) return nil, errors.Wrap(err, "REST request failed")
}
if rsp.StatusCode != http.StatusOK {
rsp.Body.Close()
return nil, r.statusCodeToError("list routes", rsp)
}
return rsp, nil
} }
return fetchExhaustively[DetailedRoute](fetchFn)
return nil, r.statusCodeToError("list routes", resp)
} }
// AddRoute calls the Tunnelstore POST endpoint for a given route. // AddRoute calls the Tunnelstore POST endpoint for a given route.
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) { func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
endpoint := r.baseEndpoints.accountRoutes endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String())) endpoint.Path = path.Join(endpoint.Path)
resp, err := r.sendRequest("POST", endpoint, newRoute) resp, err := r.sendRequest("POST", endpoint, newRoute)
if err != nil { if err != nil {
return Route{}, errors.Wrap(err, "REST request failed") return Route{}, errors.Wrap(err, "REST request failed")
@ -173,10 +175,9 @@ func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
} }
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route. // DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error { func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
endpoint := r.baseEndpoints.accountRoutes endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String())) endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
setVnetParam(&endpoint, params.VNetID)
resp, err := r.sendRequest("DELETE", endpoint, nil) resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil { if err != nil {
@ -211,12 +212,6 @@ func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp) return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
} }
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
var routes []*DetailedRoute
err := parseResponse(body, &routes)
return routes, err
}
func parseRoute(body io.ReadCloser) (Route, error) { func parseRoute(body io.ReadCloser) (Route, error) {
var route Route var route Route
err := parseResponse(body, &route) err := parseResponse(body, &route)

View File

@ -58,31 +58,29 @@ type IpRouteFilter struct {
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied. // NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) { func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
f := &IpRouteFilter{ f := NewIPRouteFilter()
queryParams: url.Values{},
}
// Set deletion filter // Set deletion filter
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) { if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
f.deleted() f.Deleted()
} else { } else {
f.notDeleted() f.NotDeleted()
} }
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil { if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
return nil, err return nil, err
} else if subset != nil { } else if subset != nil {
f.networkIsSupersetOf(*subset) f.NetworkIsSupersetOf(*subset)
} }
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil { if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
return nil, err return nil, err
} else if superset != nil { } else if superset != nil {
f.networkIsSupersetOf(*superset) f.NetworkIsSupersetOf(*superset)
} }
if comment := c.String(filterIpRouteComment.Name); comment != "" { if comment := c.String(filterIpRouteComment.Name); comment != "" {
f.commentIs(comment) f.CommentIs(comment)
} }
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" { if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
@ -90,7 +88,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name) return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
} }
f.tunnelID(u) f.TunnelID(u)
} }
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" { if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
@ -98,7 +96,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name) return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
} }
f.vnetID(u) f.VNetID(u)
} }
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 { if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
@ -124,35 +122,44 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
return subset, nil return subset, nil
} }
func (f *IpRouteFilter) commentIs(comment string) { func NewIPRouteFilter() *IpRouteFilter {
values := &IpRouteFilter{queryParams: url.Values{}}
// always list cfd_tunnel routes only
values.queryParams.Set("tun_types", "cfd_tunnel")
return values
}
func (f *IpRouteFilter) CommentIs(comment string) {
f.queryParams.Set("comment", comment) f.queryParams.Set("comment", comment)
} }
func (f *IpRouteFilter) notDeleted() { func (f *IpRouteFilter) NotDeleted() {
f.queryParams.Set("is_deleted", "false") f.queryParams.Set("is_deleted", "false")
} }
func (f *IpRouteFilter) deleted() { func (f *IpRouteFilter) Deleted() {
f.queryParams.Set("is_deleted", "true") f.queryParams.Set("is_deleted", "true")
} }
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) { func (f *IpRouteFilter) NetworkIsSubsetOf(superset net.IPNet) {
f.queryParams.Set("network_subset", superset.String()) f.queryParams.Set("network_subset", superset.String())
} }
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) { func (f *IpRouteFilter) NetworkIsSupersetOf(subset net.IPNet) {
f.queryParams.Set("network_superset", subset.String()) f.queryParams.Set("network_superset", subset.String())
} }
func (f *IpRouteFilter) existedAt(existedAt time.Time) { func (f *IpRouteFilter) ExistedAt(existedAt time.Time) {
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339)) f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
} }
func (f *IpRouteFilter) tunnelID(id uuid.UUID) { func (f *IpRouteFilter) TunnelID(id uuid.UUID) {
f.queryParams.Set("tunnel_id", id.String()) f.queryParams.Set("tunnel_id", id.String())
} }
func (f *IpRouteFilter) vnetID(id uuid.UUID) { func (f *IpRouteFilter) VNetID(id uuid.UUID) {
f.queryParams.Set("virtual_network_id", id.String()) f.queryParams.Set("virtual_network_id", id.String())
} }
@ -160,6 +167,10 @@ func (f *IpRouteFilter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max))) f.queryParams.Set("per_page", strconv.Itoa(int(max)))
} }
func (f *IpRouteFilter) Page(page int) {
f.queryParams.Set("page", strconv.Itoa(page))
}
func (f IpRouteFilter) Encode() string { func (f IpRouteFilter) Encode() string {
return f.queryParams.Encode() return f.queryParams.Encode()
} }

View File

@ -69,6 +69,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
}{ }{
{ {
`{ `{
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
"network":"10.1.2.40/29", "network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8", "tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"comment":"test", "comment":"test",
@ -80,6 +81,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
}, },
{ {
`{ `{
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
"network":"10.1.2.40/29", "network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8", "tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9", "virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
@ -167,9 +169,10 @@ func TestRouteTableString(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, network) require.NotNil(t, network)
r := DetailedRoute{ r := DetailedRoute{
ID: uuid.Nil,
Network: CIDR(*network), Network: CIDR(*network),
} }
row := r.TableString() row := r.TableString()
fmt.Println(row) fmt.Println(row)
require.True(t, strings.HasPrefix(row, "1.2.3.4/32")) require.True(t, strings.HasPrefix(row, "00000000-0000-0000-0000-000000000000\t1.2.3.4/32"))
} }

View File

@ -93,7 +93,7 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWith
switch resp.StatusCode { switch resp.StatusCode {
case http.StatusOK: case http.StatusOK:
var tunnel TunnelWithToken var tunnel TunnelWithToken
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil { if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
return nil, serdeErr return nil, serdeErr
} }
return &tunnel, nil return &tunnel, nil
@ -159,9 +159,14 @@ func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err e
return "", r.statusCodeToError("get tunnel token", resp) return "", r.statusCodeToError("get tunnel token", resp)
} }
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error { func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
endpoint := r.baseEndpoints.accountLevel endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID)) endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
// Cascade will delete all tunnel dependencies (connections, routes, etc.) that
// are linked to the deleted tunnel.
if cascade {
endpoint.RawQuery = "cascade=true"
}
resp, err := r.sendRequest("DELETE", endpoint, nil) resp, err := r.sendRequest("DELETE", endpoint, nil)
if err != nil { if err != nil {
return errors.Wrap(err, "REST request failed") return errors.Wrap(err, "REST request failed")
@ -172,25 +177,22 @@ func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
} }
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) { func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel fetchFn := func(page int) (*http.Response, error) {
endpoint.RawQuery = filter.encode() endpoint := r.baseEndpoints.accountLevel
resp, err := r.sendRequest("GET", endpoint, nil) filter.Page(page)
if err != nil { endpoint.RawQuery = filter.encode()
return nil, errors.Wrap(err, "REST request failed") rsp, err := r.sendRequest("GET", endpoint, nil)
} if err != nil {
defer resp.Body.Close() return nil, errors.Wrap(err, "REST request failed")
}
if resp.StatusCode == http.StatusOK { if rsp.StatusCode != http.StatusOK {
return parseListTunnels(resp.Body) rsp.Body.Close()
return nil, r.statusCodeToError("list tunnels", rsp)
}
return rsp, nil
} }
return nil, r.statusCodeToError("list tunnels", resp) return fetchExhaustively[Tunnel](fetchFn)
}
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
var tunnels []*Tunnel
err := parseResponse(body, &tunnels)
return tunnels, err
} }
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) { func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {

View File

@ -50,6 +50,10 @@ func (f *TunnelFilter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max))) f.queryParams.Set("per_page", strconv.Itoa(int(max)))
} }
func (f *TunnelFilter) Page(page int) {
f.queryParams.Set("page", strconv.Itoa(page))
}
func (f TunnelFilter) encode() string { func (f TunnelFilter) encode() string {
return f.queryParams.Encode() return f.queryParams.Encode()
} }

View File

@ -3,7 +3,6 @@ package cfapi
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"net" "net"
"reflect" "reflect"
"strings" "strings"
@ -16,52 +15,6 @@ import (
var loc, _ = time.LoadLocation("UTC") var loc, _ = time.LoadLocation("UTC")
func Test_parseListTunnels(t *testing.T) {
type args struct {
body string
}
tests := []struct {
name string
args args
want []*Tunnel
wantErr bool
}{
{
name: "empty list",
args: args{body: `{"success": true, "result": []}`},
want: []*Tunnel{},
},
{
name: "success is false",
args: args{body: `{"success": false, "result": []}`},
wantErr: true,
},
{
name: "errors are present",
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
wantErr: true,
},
{
name: "invalid response",
args: args{body: `abc`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := io.NopCloser(bytes.NewReader([]byte(tt.args.body)))
got, err := parseListTunnels(body)
if (err != nil) != tt.wantErr {
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
}
})
}
}
func Test_unmarshalTunnel(t *testing.T) { func Test_unmarshalTunnel(t *testing.T) {
type args struct { type args struct {
body string body string

View File

@ -1,95 +1,70 @@
pinned_go: &pinned_go go=1.19.6-1 pinned_go: &pinned_go go-boring=1.22.10-1
pinned_go_fips: &pinned_go_fips go-boring=1.19.6-1
build_dir: &build_dir /cfsetup_build build_dir: &build_dir /cfsetup_build
default-flavor: bullseye default-flavor: bookworm
buster: &buster
build: bullseye: &bullseye
build-linux:
build_dir: *build_dir build_dir: *build_dir
builddeps: &build_deps builddeps: &build_deps
- *pinned_go - *pinned_go
- build-essential - build-essential
- gotest-to-teamcity - fakeroot
- rubygem-fpm
- rpm
- libffi-dev
- golangci-lint
pre-cache: &build_pre_cache pre-cache: &build_pre_cache
- export GOCACHE=/cfsetup_build/.cache/go-build - export GOCACHE=/cfsetup_build/.cache/go-build
- go install golang.org/x/tools/cmd/goimports@latest - go install golang.org/x/tools/cmd/goimports@v0.30.0
post-cache: post-cache:
- export GOOS=linux # Linting
- export GOARCH=amd64 - make lint
- make cloudflared - make fmt-check
build-fips: # Build binary for component test
- GOOS=linux GOARCH=amd64 make cloudflared
build-linux-fips:
build_dir: *build_dir build_dir: *build_dir
builddeps: &build_deps_fips builddeps: *build_deps
- *pinned_go_fips
- build-essential
- gotest-to-teamcity
pre-cache: *build_pre_cache pre-cache: *build_pre_cache
post-cache: post-cache:
- export GOOS=linux
- export GOARCH=amd64
- export FIPS=true - export FIPS=true
- make cloudflared # Build binary for component test
- GOOS=linux GOARCH=amd64 make cloudflared
cover: cover:
build_dir: *build_dir build_dir: *build_dir
builddeps: *build_deps builddeps: *build_deps
pre-cache: *build_pre_cache pre-cache: *build_pre_cache
post-cache: post-cache:
- make cover - make cover
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64) # except FIPS and macos
github-release-pkgs: build-linux-release:
build_dir: *build_dir build_dir: *build_dir
builddeps: builddeps: &build_deps_release
- *pinned_go - *pinned_go
- build-essential - build-essential
- fakeroot - fakeroot
- rubygem-fpm - rubygem-fpm
- rpm - rpm
- wget
# libmsi and libgcab are libraries the wixl binary depends on.
- libmsi-dev
- libgcab-dev
- python3-dev
- libffi-dev - libffi-dev
- python3-setuptools - python3-dev
- python3-pip - python3-pip
- reprepro - python3-setuptools
- createrepo - wget
pre-cache: &github_release_pkgs_pre_cache - python3-venv
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
- chmod a+x /usr/local/bin/wixl
- pip3 install pynacl==1.4.0
- pip3 install pygithub==1.55
- pip3 install boto3==1.22.9
- pip3 install python-gnupg==0.4.9
post-cache: post-cache:
- python3 -m venv env
- . /cfsetup_build/env/bin/activate
- pip install pynacl==1.4.0 pygithub==1.55 boto3==1.22.9 python-gnupg==0.4.9
# build all packages (except macos and FIPS) and move them to /cfsetup/built_artifacts # build all packages (except macos and FIPS) and move them to /cfsetup/built_artifacts
- ./build-packages.sh - ./build-packages.sh
# release the packages built and moved to /cfsetup/built_artifacts
- make github-release-built-pkgs
# publish packages to linux repos
- make release-pkgs-linux
# handle FIPS separately so that we built with gofips compiler # handle FIPS separately so that we built with gofips compiler
github-fips-release-pkgs: build-linux-fips-release:
build_dir: *build_dir build_dir: *build_dir
builddeps: builddeps: *build_deps_release
- *pinned_go_fips
- build-essential
- fakeroot
- rubygem-fpm
- rpm
- wget
# libmsi and libgcab are libraries the wixl binary depends on.
- libmsi-dev
- libgcab-dev
- python3-dev
- libffi-dev
- python3-setuptools
- python3-pip
pre-cache: *github_release_pkgs_pre_cache
post-cache: post-cache:
# same logic as above, but for FIPS packages only # same logic as above, but for FIPS packages only
- ./build-packages-fips.sh - ./build-packages-fips.sh
- make github-release-built-pkgs
generate-versions-file: generate-versions-file:
build_dir: *build_dir build_dir: *build_dir
builddeps: builddeps:
@ -111,7 +86,7 @@ buster: &buster
build-fips-internal-deb: build-fips-internal-deb:
build_dir: *build_dir build_dir: *build_dir
builddeps: &build_fips_deb_deps builddeps: &build_fips_deb_deps
- *pinned_go_fips - *pinned_go
- build-essential - build-essential
- fakeroot - fakeroot
- rubygem-fpm - rubygem-fpm
@ -138,7 +113,7 @@ buster: &buster
- export GOOS=linux - export GOOS=linux
- export GOARCH=arm64 - export GOARCH=arm64
- export NIGHTLY=true - export NIGHTLY=true
#- export FIPS=true # TUN-7595 # - export FIPS=true # TUN-7595
- export ORIGINAL_NAME=true - export ORIGINAL_NAME=true
- make cloudflared-deb - make cloudflared-deb
build-deb-arm64: build-deb-arm64:
@ -148,21 +123,7 @@ buster: &buster
- export GOOS=linux - export GOOS=linux
- export GOARCH=arm64 - export GOARCH=arm64
- make cloudflared-deb - make cloudflared-deb
github-release-macos-amd64: package-windows:
build_dir: *build_dir
builddeps: &build_pygithub
- *pinned_go
- build-essential
- python3-dev
- libffi-dev
- python3-setuptools
- python3-pip
pre-cache: &install_pygithub
- pip3 install pynacl==1.4.0
- pip3 install pygithub==1.55
post-cache:
- make github-mac-upload
github-release-windows:
build_dir: *build_dir build_dir: *build_dir
builddeps: builddeps:
- *pinned_go - *pinned_go
@ -175,85 +136,122 @@ buster: &buster
# libmsi and libgcab are libraries the wixl binary depends on. # libmsi and libgcab are libraries the wixl binary depends on.
- libmsi-dev - libmsi-dev
- libgcab-dev - libgcab-dev
- python3-venv
pre-cache: pre-cache:
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin - wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
- chmod a+x /usr/local/bin/wixl - chmod a+x /usr/local/bin/wixl
- pip3 install pynacl==1.4.0
- pip3 install pygithub==1.55
post-cache: post-cache:
- python3 -m venv env
- . env/bin/activate
- pip install pynacl==1.4.0 pygithub==1.55
- .teamcity/package-windows.sh - .teamcity/package-windows.sh
- make github-windows-upload
test: test:
build_dir: *build_dir build_dir: *build_dir
builddeps: *build_deps builddeps: &build_deps_tests
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
- rpm
- libffi-dev
- gotest-to-teamcity
pre-cache: *build_pre_cache pre-cache: *build_pre_cache
post-cache: post-cache:
- export GOOS=linux - export GOOS=linux
- export GOARCH=amd64 - export GOARCH=amd64
- export PATH="$HOME/go/bin:$PATH" - export PATH="$HOME/go/bin:$PATH"
- ./fmt-check.sh
- make test | gotest-to-teamcity - make test | gotest-to-teamcity
test-fips: test-fips:
build_dir: *build_dir build_dir: *build_dir
builddeps: *build_deps_fips builddeps: *build_deps_tests
pre-cache: *build_pre_cache pre-cache: *build_pre_cache
post-cache: post-cache:
- export GOOS=linux - export GOOS=linux
- export GOARCH=amd64 - export GOARCH=amd64
- export FIPS=true - export FIPS=true
- export PATH="$HOME/go/bin:$PATH" - export PATH="$HOME/go/bin:$PATH"
- ./fmt-check.sh
- make test | gotest-to-teamcity - make test | gotest-to-teamcity
component-test: component-test:
build_dir: *build_dir build_dir: *build_dir
builddeps: builddeps: &build_deps_component_test
- *pinned_go - *pinned_go
- python3.7 - python3
- python3-pip - python3-pip
- python3-setuptools - python3-setuptools
# procps installs the ps command which is needed in test_sysv_service because the init script # procps installs the ps command which is needed in test_sysv_service
# uses ps pid to determine if the agent is running # because the init script uses ps pid to determine if the agent is
# running
- procps - procps
- python3-venv
pre-cache-copy-paths: pre-cache-copy-paths:
- component-tests/requirements.txt - component-tests/requirements.txt
pre-cache: &component_test_pre_cache
- sudo pip3 install --upgrade -r component-tests/requirements.txt
post-cache: &component_test_post_cache post-cache: &component_test_post_cache
# Creates and routes a Named Tunnel for this build. Also constructs config file from env vars. - python3 -m venv env
- . env/bin/activate
- pip install --upgrade -r component-tests/requirements.txt
# Creates and routes a Named Tunnel for this build. Also constructs
# config file from env vars.
- python3 component-tests/setup.py --type create - python3 component-tests/setup.py --type create
- pytest component-tests -o log_cli=true --log-cli-level=INFO - pytest component-tests -o log_cli=true --log-cli-level=INFO
# The Named Tunnel is deleted and its route unprovisioned here. # The Named Tunnel is deleted and its route unprovisioned here.
- python3 component-tests/setup.py --type cleanup - python3 component-tests/setup.py --type cleanup
component-test-fips: component-test-fips:
build_dir: *build_dir build_dir: *build_dir
builddeps: builddeps: *build_deps_component_test
- *pinned_go_fips
- python3.7
- python3-pip
- python3-setuptools
# procps installs the ps command which is needed in test_sysv_service because the init script
# uses ps pid to determine if the agent is running
- procps
pre-cache-copy-paths: pre-cache-copy-paths:
- component-tests/requirements.txt - component-tests/requirements.txt
pre-cache: *component_test_pre_cache
post-cache: *component_test_post_cache post-cache: *component_test_post_cache
update-homebrew: github-release-dryrun:
builddeps:
- openssh-client
- s3cmd
- jq
- build-essential
- procps
post-cache:
- .teamcity/update-homebrew.sh
- .teamcity/update-homebrew-core.sh
github-message-release:
build_dir: *build_dir build_dir: *build_dir
builddeps: *build_pygithub builddeps:
pre-cache: *install_pygithub - *pinned_go
- build-essential
- python3-dev
- libffi-dev
- python3-setuptools
- python3-pip
- python3-venv
post-cache: post-cache:
- make github-message - python3 -m venv env
- . env/bin/activate
- pip install pynacl==1.4.0 pygithub==1.55
- make github-release-dryrun
github-release:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- python3-dev
- libffi-dev
- python3-setuptools
- python3-pip
- python3-venv
post-cache:
- python3 -m venv env
- . env/bin/activate
- pip install pynacl==1.4.0 pygithub==1.55
- make github-release
r2-linux-release:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
- rpm
- wget
- python3-dev
- libffi-dev
- python3-setuptools
- python3-pip
- reprepro
- createrepo-c
- python3-venv
post-cache:
- python3 -m venv env
- . env/bin/activate
- pip install pynacl==1.4.0 pygithub==1.55 boto3==1.22.9 python-gnupg==0.4.9
- make r2-linux-release
bullseye: *buster bookworm: *bullseye
bookworm: *buster trixie: *bullseye

View File

@ -46,7 +46,7 @@
<!--Set the cloudflared bin location to the Path Environment Variable--> <!--Set the cloudflared bin location to the Path Environment Variable-->
<Environment Id="ENV0" <Environment Id="ENV0"
Name="PATH" Name="PATH"
Value="[INSTALLDIR]." Value="[INSTALLDIR]"
Permanent="no" Permanent="no"
Part="last" Part="last"
Action="create" Action="create"

View File

@ -70,14 +70,14 @@ func ssh(c *cli.Context) error {
// get the hostname from the cmdline and error out if its not provided // get the hostname from the cmdline and error out if its not provided
rawHostName := c.String(sshHostnameFlag) rawHostName := c.String(sshHostnameFlag)
hostname, err := validation.ValidateHostname(rawHostName) url, err := parseURL(rawHostName)
if err != nil || rawHostName == "" { if err != nil {
log.Err(err).Send()
return cli.ShowCommandHelp(c, "ssh") return cli.ShowCommandHelp(c, "ssh")
} }
originURL := ensureURLScheme(hostname)
// get the headers from the cmdline and add them // get the headers from the cmdline and add them
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag)) headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) { if c.IsSet(sshTokenIDFlag) {
headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag)) headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
} }
@ -89,9 +89,9 @@ func ssh(c *cli.Context) error {
carrier.SetBastionDest(headers, c.String(sshDestinationFlag)) carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
options := &carrier.StartOptions{ options := &carrier.StartOptions{
OriginURL: originURL, OriginURL: url.String(),
Headers: headers, Headers: headers,
Host: hostname, Host: url.Host,
} }
if connectTo := c.String(sshConnectTo); connectTo != "" { if connectTo := c.String(sshConnectTo); connectTo != "" {
@ -104,7 +104,7 @@ func ssh(c *cli.Context) error {
case 3: case 3:
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[2], parts[1]) options.OriginURL = fmt.Sprintf("https://%s:%s", parts[2], parts[1])
options.TLSClientConfig = &tls.Config{ options.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true, // #nosec G402
ServerName: parts[0], ServerName: parts[0],
} }
log.Warn().Msgf("Using insecure SSL connection because SNI overridden to %s", parts[0]) log.Warn().Msgf("Using insecure SSL connection because SNI overridden to %s", parts[0])
@ -138,20 +138,8 @@ func ssh(c *cli.Context) error {
// default to 10 if provided but unset // default to 10 if provided but unset
maxMessages = 10 maxMessages = 10
} }
logger := log.With().Str("host", hostname).Logger() logger := log.With().Str("host", url.Host).Logger()
s = stream.NewDebugStream(s, &logger, maxMessages) s = stream.NewDebugStream(s, &logger, maxMessages)
} }
carrier.StartClient(wsConn, s, options) return carrier.StartClient(wsConn, s, options)
return nil
}
func buildRequestHeaders(values []string) http.Header {
headers := make(http.Header)
for _, valuePair := range values {
header, value, found := strings.Cut(valuePair, ":")
if found {
headers.Add(strings.TrimSpace(header), strings.TrimSpace(value))
}
}
return headers
} }

View File

@ -1,19 +0,0 @@
package access
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBuildRequestHeaders(t *testing.T) {
headers := make(http.Header)
headers.Add("client", "value")
headers.Add("secret", "safe-value")
values := buildRequestHeaders([]string{"client: value", "secret: safe-value", "trash", "cf-trace-id: 000:000:0:1:asd"})
assert.Equal(t, headers.Get("client"), values.Get("client"))
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
assert.Equal(t, headers.Get("cf-trace-id"), values.Get("000:000:0:1:asd"))
}

View File

@ -19,6 +19,7 @@ import (
"github.com/cloudflare/cloudflared/carrier" "github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/sshgen" "github.com/cloudflare/cloudflared/sshgen"
"github.com/cloudflare/cloudflared/token" "github.com/cloudflare/cloudflared/token"
@ -26,6 +27,8 @@ import (
) )
const ( const (
appURLFlag = "app"
loginQuietFlag = "quiet"
sshHostnameFlag = "hostname" sshHostnameFlag = "hostname"
sshDestinationFlag = "destination" sshDestinationFlag = "destination"
sshURLFlag = "url" sshURLFlag = "url"
@ -82,14 +85,29 @@ func Commands() []*cli.Command {
applications from the command line.`, applications from the command line.`,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
{ {
Name: "login", Name: "login",
Action: cliutil.Action(login), Action: cliutil.Action(login),
Usage: "login <url of access application>", Usage: "login <url of access application>",
ArgsUsage: "url of Access application",
Description: `The login subcommand initiates an authentication flow with your identity provider. Description: `The login subcommand initiates an authentication flow with your identity provider.
The subcommand will launch a browser. For headless systems, a url is provided. The subcommand will launch a browser. For headless systems, a url is provided.
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT) Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
scoped to your identity, the application you intend to reach, and valid for a session duration set by your scoped to your identity, the application you intend to reach, and valid for a session duration set by your
administrator. cloudflared stores the token in local storage.`, administrator. cloudflared stores the token in local storage.`,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: loginQuietFlag,
Aliases: []string{"q"},
Usage: "do not print the jwt to the command line",
},
&cli.BoolFlag{
Name: "no-verbose",
Usage: "print only the jwt to stdout",
},
&cli.StringFlag{
Name: appURLFlag,
},
},
}, },
{ {
Name: "curl", Name: "curl",
@ -103,12 +121,12 @@ func Commands() []*cli.Command {
{ {
Name: "token", Name: "token",
Action: cliutil.Action(generateToken), Action: cliutil.Action(generateToken),
Usage: "token -app=<url of access application>", Usage: "token <url of access application>",
ArgsUsage: "url of Access application", ArgsUsage: "url of Access application",
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`, Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "app", Name: appURLFlag,
}, },
}, },
}, },
@ -124,15 +142,18 @@ func Commands() []*cli.Command {
Name: sshHostnameFlag, Name: sshHostnameFlag,
Aliases: []string{"tunnel-host", "T"}, Aliases: []string{"tunnel-host", "T"},
Usage: "specify the hostname of your application.", Usage: "specify the hostname of your application.",
EnvVars: []string{"TUNNEL_SERVICE_HOSTNAME"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: sshDestinationFlag, Name: sshDestinationFlag,
Usage: "specify the destination address of your SSH server.", Usage: "specify the destination address of your SSH server.",
EnvVars: []string{"TUNNEL_SERVICE_DESTINATION"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: sshURLFlag, Name: sshURLFlag,
Aliases: []string{"listener", "L"}, Aliases: []string{"listener", "L"},
Usage: "specify the host:port to forward data to Cloudflare edge.", Usage: "specify the host:port to forward data to Cloudflare edge.",
EnvVars: []string{"TUNNEL_SERVICE_URL"},
}, },
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: sshHeaderFlag, Name: sshHeaderFlag,
@ -152,15 +173,15 @@ func Commands() []*cli.Command {
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"}, EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: logger.LogFileFlag, Name: cfdflags.LogFile,
Usage: "Save application log to this file for reporting issues.", Usage: "Save application log to this file for reporting issues.",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: logger.LogSSHDirectoryFlag, Name: cfdflags.LogDirectory,
Usage: "Save application log to this directory for reporting issues.", Usage: "Save application log to this directory for reporting issues.",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: logger.LogSSHLevelFlag, Name: cfdflags.LogLevelSSH,
Aliases: []string{"loglevel"}, //added to match the tunnel side Aliases: []string{"loglevel"}, //added to match the tunnel side
Usage: "Application logging level {debug, info, warn, error, fatal}. ", Usage: "Application logging level {debug, info, warn, error, fatal}. ",
}, },
@ -221,10 +242,8 @@ func login(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog) log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
args := c.Args() appURL, err := getAppURLFromArgs(c)
rawURL := ensureURLScheme(args.First()) if err != nil {
appURL, err := url.Parse(rawURL)
if args.Len() < 1 || err != nil {
log.Error().Msg("Please provide the url of the Access application") log.Error().Msg("Please provide the url of the Access application")
return err return err
} }
@ -247,21 +266,22 @@ func login(c *cli.Context) error {
fmt.Fprintln(os.Stderr, "token for provided application was empty.") fmt.Fprintln(os.Stderr, "token for provided application was empty.")
return errors.New("empty application token") return errors.New("empty application token")
} }
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
if c.Bool(loginQuietFlag) {
return nil
}
// Chatty by default for backward compat. The new --app flag
// is an implicit opt-out of the backwards-compatible chatty output.
if c.Bool("no-verbose") || c.IsSet(appURLFlag) {
fmt.Fprint(os.Stdout, cfdToken)
} else {
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
}
return nil return nil
} }
// ensureURLScheme prepends a URL with https:// if it doesn't have a scheme. http:// URLs will not be converted.
func ensureURLScheme(url string) string {
url = strings.Replace(strings.ToLower(url), "http://", "https://", 1)
if !strings.HasPrefix(url, "https://") {
url = fmt.Sprintf("https://%s", url)
}
return url
}
// curl provides a wrapper around curl, passing Access JWT along in request // curl provides a wrapper around curl, passing Access JWT along in request
func curl(c *cli.Context) error { func curl(c *cli.Context) error {
err := sentry.Init(sentry.ClientOptions{ err := sentry.Init(sentry.ClientOptions{
@ -323,7 +343,7 @@ func run(cmd string, args ...string) error {
return err return err
} }
go func() { go func() {
io.Copy(os.Stderr, stderr) _, _ = io.Copy(os.Stderr, stderr)
}() }()
stdout, err := c.StdoutPipe() stdout, err := c.StdoutPipe()
@ -331,11 +351,22 @@ func run(cmd string, args ...string) error {
return err return err
} }
go func() { go func() {
io.Copy(os.Stdout, stdout) _, _ = io.Copy(os.Stdout, stdout)
}() }()
return c.Run() return c.Run()
} }
func getAppURLFromArgs(c *cli.Context) (*url.URL, error) {
var appURLStr string
args := c.Args()
if args.Len() < 1 {
appURLStr = c.String(appURLFlag)
} else {
appURLStr = args.First()
}
return parseURL(appURLStr)
}
// token dumps provided token to stdout // token dumps provided token to stdout
func generateToken(c *cli.Context) error { func generateToken(c *cli.Context) error {
err := sentry.Init(sentry.ClientOptions{ err := sentry.Init(sentry.ClientOptions{
@ -345,8 +376,8 @@ func generateToken(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
appURL, err := url.Parse(ensureURLScheme(c.String("app"))) appURL, err := getAppURLFromArgs(c)
if err != nil || c.NumFlags() < 1 { if err != nil {
fmt.Fprintln(os.Stderr, "Please provide a url.") fmt.Fprintln(os.Stderr, "Please provide a url.")
return err return err
} }
@ -398,7 +429,7 @@ func sshGen(c *cli.Context) error {
return cli.ShowCommandHelp(c, "ssh-gen") return cli.ShowCommandHelp(c, "ssh-gen")
} }
originURL, err := url.Parse(ensureURLScheme(hostname)) originURL, err := parseURL(hostname)
if err != nil { if err != nil {
return err return err
} }
@ -477,6 +508,11 @@ func processURL(s string) (*url.URL, error) {
// cloudflaredPath pulls the full path of cloudflared on disk // cloudflaredPath pulls the full path of cloudflared on disk
func cloudflaredPath() string { func cloudflaredPath() string {
path, err := os.Executable()
if err == nil && isFileThere(path) {
return path
}
for _, p := range strings.Split(os.Getenv("PATH"), ":") { for _, p := range strings.Split(os.Getenv("PATH"), ":") {
path := fmt.Sprintf("%s/%s", p, "cloudflared") path := fmt.Sprintf("%s/%s", p, "cloudflared")
if isFileThere(path) { if isFileThere(path) {
@ -496,10 +532,10 @@ func isFileThere(candidate string) bool {
} }
// verifyTokenAtEdge checks for a token on disk, or generates a new one. // verifyTokenAtEdge checks for a token on disk, or generates a new one.
// Then makes a request to to the origin with the token to ensure it is valid. // Then makes a request to the origin with the token to ensure it is valid.
// Returns nil if token is valid. // Returns nil if token is valid.
func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error { func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, log *zerolog.Logger) error {
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag)) headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) { if c.IsSet(sshTokenIDFlag) {
headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag)) headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
} }

View File

@ -1,25 +0,0 @@
package access
import "testing"
func Test_ensureURLScheme(t *testing.T) {
type args struct {
url string
}
tests := []struct {
name string
args args
want string
}{
{"no scheme", args{"localhost:123"}, "https://localhost:123"},
{"http scheme", args{"http://test"}, "https://test"},
{"https scheme", args{"https://test"}, "https://test"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ensureURLScheme(tt.args.url); got != tt.want {
t.Errorf("ensureURLScheme() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,55 @@
package access
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"golang.org/x/net/http/httpguts"
)
// parseRequestHeaders will take user-provided header values as strings "Content-Type: application/json" and create
// a http.Header object.
func parseRequestHeaders(values []string) http.Header {
headers := make(http.Header)
for _, valuePair := range values {
header, value, found := strings.Cut(valuePair, ":")
if found {
headers.Add(strings.TrimSpace(header), strings.TrimSpace(value))
}
}
return headers
}
// 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
func parseURL(input string) (*url.URL, error) {
if input == "" {
return nil, errors.New("no input provided")
}
if !strings.HasPrefix(input, "https://") && !strings.HasPrefix(input, "http://") {
input = fmt.Sprintf("https://%s", input)
}
url, err := url.ParseRequestURI(input)
if err != nil {
return nil, fmt.Errorf("failed to parse as URL: %w", err)
}
if url.Scheme != "https" {
url.Scheme = "https"
}
if url.Host == "" {
return nil, errors.New("failed to parse Host")
}
host, err := httpguts.PunycodeHostPort(url.Host)
if err != nil || host == "" {
return nil, err
}
if !httpguts.ValidHostHeader(host) {
return nil, errors.New("invalid Host provided")
}
url.Host = host
return url, nil
}

View File

@ -0,0 +1,80 @@
package access
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseRequestHeaders(t *testing.T) {
values := parseRequestHeaders([]string{"client: value", "secret: safe-value", "trash", "cf-trace-id: 000:000:0:1:asd"})
assert.Len(t, values, 3)
assert.Equal(t, "value", values.Get("client"))
assert.Equal(t, "safe-value", values.Get("secret"))
assert.Equal(t, "000:000:0:1:asd", values.Get("cf-trace-id"))
}
func TestParseURL(t *testing.T) {
schemes := []string{
"http://",
"https://",
"",
}
hosts := []struct {
input string
expected string
}{
{"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"},
{":8080", ":8080"},
{"example.com", "example.com"},
{"hello.example.com", "hello.example.com"},
{"bücher.example.com", "xn--bcher-kva.example.com"},
}
paths := []string{
"",
"/test",
"/example.com?qwe=123",
}
for i, scheme := range schemes {
for j, host := range hosts {
for k, path := range paths {
t.Run(fmt.Sprintf("%d_%d_%d", i, j, k), func(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)
assert.Equal(t, expected, url.String())
assert.Equal(t, host.expected, url.Host)
assert.Equal(t, "https", url.Scheme)
})
}
}
}
t.Run("no input", func(t *testing.T) {
_, err := parseURL("")
assert.ErrorContains(t, err, "no input provided")
})
t.Run("missing host", func(t *testing.T) {
_, err := parseURL("https:///host")
assert.ErrorContains(t, err, "failed to parse Host")
})
t.Run("invalid path only", func(t *testing.T) {
_, err := parseURL("/host")
assert.ErrorContains(t, err, "failed to parse Host")
})
t.Run("invalid parse URL", func(t *testing.T) {
_, err := parseURL("https://host\\host")
assert.ErrorContains(t, err, "failed to parse as URL")
})
}

View File

@ -1,7 +1,10 @@
package cliutil package cliutil
import ( import (
"crypto/sha256"
"fmt" "fmt"
"io"
"os"
"runtime" "runtime"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -13,6 +16,7 @@ type BuildInfo struct {
GoArch string `json:"go_arch"` GoArch string `json:"go_arch"`
BuildType string `json:"build_type"` BuildType string `json:"build_type"`
CloudflaredVersion string `json:"cloudflared_version"` CloudflaredVersion string `json:"cloudflared_version"`
Checksum string `json:"checksum"`
} }
func GetBuildInfo(buildType, version string) *BuildInfo { func GetBuildInfo(buildType, version string) *BuildInfo {
@ -22,11 +26,12 @@ func GetBuildInfo(buildType, version string) *BuildInfo {
GoArch: runtime.GOARCH, GoArch: runtime.GOARCH,
BuildType: buildType, BuildType: buildType,
CloudflaredVersion: version, CloudflaredVersion: version,
Checksum: currentBinaryChecksum(),
} }
} }
func (bi *BuildInfo) Log(log *zerolog.Logger) { func (bi *BuildInfo) Log(log *zerolog.Logger) {
log.Info().Msgf("Version %s", bi.CloudflaredVersion) log.Info().Msgf("Version %s (Checksum %s)", bi.CloudflaredVersion, bi.Checksum)
if bi.BuildType != "" { if bi.BuildType != "" {
log.Info().Msgf("Built%s", bi.GetBuildTypeMsg()) log.Info().Msgf("Built%s", bi.GetBuildTypeMsg())
} }
@ -51,3 +56,28 @@ func (bi *BuildInfo) GetBuildTypeMsg() string {
func (bi *BuildInfo) UserAgent() string { func (bi *BuildInfo) UserAgent() string {
return fmt.Sprintf("cloudflared/%s", bi.CloudflaredVersion) return fmt.Sprintf("cloudflared/%s", bi.CloudflaredVersion)
} }
// FileChecksum opens a file and returns the SHA256 checksum.
func FileChecksum(filePath string) (string, error) {
f, err := os.Open(filePath)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func currentBinaryChecksum() string {
currentPath, err := os.Executable()
if err != nil {
return ""
}
sum, _ := FileChecksum(currentPath)
return sum
}

View File

@ -4,7 +4,7 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc" "github.com/urfave/cli/v2/altsrc"
"github.com/cloudflare/cloudflared/logger" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
) )
var ( var (
@ -15,14 +15,14 @@ var (
func ConfigureLoggingFlags(shouldHide bool) []cli.Flag { func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{ return []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: logger.LogLevelFlag, Name: cfdflags.LogLevel,
Value: "info", Value: "info",
Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning, Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning,
EnvVars: []string{"TUNNEL_LOGLEVEL"}, EnvVars: []string{"TUNNEL_LOGLEVEL"},
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: logger.LogTransportLevelFlag, Name: cfdflags.TransportLogLevel,
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
Value: "info", Value: "info",
Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}", Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}",
@ -30,19 +30,19 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: logger.LogFileFlag, Name: cfdflags.LogFile,
Usage: "Save application log to this file for reporting issues.", Usage: "Save application log to this file for reporting issues.",
EnvVars: []string{"TUNNEL_LOGFILE"}, EnvVars: []string{"TUNNEL_LOGFILE"},
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: logger.LogDirectoryFlag, Name: cfdflags.LogDirectory,
Usage: "Save application log to this directory for reporting issues.", Usage: "Save application log to this directory for reporting issues.",
EnvVars: []string{"TUNNEL_LOGDIRECTORY"}, EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "trace-output", Name: cfdflags.TraceOutput,
Usage: "Name of trace output file, generated when cloudflared stops.", Usage: "Name of trace output file, generated when cloudflared stops.",
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"}, EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
Hidden: shouldHide, Hidden: shouldHide,

View File

@ -0,0 +1,155 @@
package flags
const (
// HaConnections specifies how many connections to make to the edge
HaConnections = "ha-connections"
// SshPort is the port on localhost the cloudflared ssh server will run on
SshPort = "local-ssh-port"
// SshIdleTimeout defines the duration a SSH session can remain idle before being closed
SshIdleTimeout = "ssh-idle-timeout"
// SshMaxTimeout defines the max duration a SSH session can remain open for
SshMaxTimeout = "ssh-max-timeout"
// SshLogUploaderBucketName is the bucket name to use for the SSH log uploader
SshLogUploaderBucketName = "bucket-name"
// SshLogUploaderRegionName is the AWS region name to use for the SSH log uploader
SshLogUploaderRegionName = "region-name"
// SshLogUploaderSecretID is the Secret id of SSH log uploader
SshLogUploaderSecretID = "secret-id"
// SshLogUploaderAccessKeyID is the Access key id of SSH log uploader
SshLogUploaderAccessKeyID = "access-key-id"
// SshLogUploaderSessionTokenID is the Session token of SSH log uploader
SshLogUploaderSessionTokenID = "session-token"
// SshLogUploaderS3URL is the S3 URL of SSH log uploader (e.g. don't use AWS s3 and use google storage bucket instead)
SshLogUploaderS3URL = "s3-url-host"
// HostKeyPath is the path of the dir to save SSH host keys too
HostKeyPath = "host-key-path"
// RpcTimeout is how long to wait for a Capnp RPC request to the edge
RpcTimeout = "rpc-timeout"
// WriteStreamTimeout sets if we should have a timeout when writing data to a stream towards the destination (edge/origin).
WriteStreamTimeout = "write-stream-timeout"
// QuicDisablePathMTUDiscovery sets if QUIC should not perform PTMU discovery and use a smaller (safe) packet size.
// Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
// Note that this may result in packet drops for UDP proxying, since we expect being able to send at least 1280 bytes of inner packets.
QuicDisablePathMTUDiscovery = "quic-disable-pmtu-discovery"
// QuicConnLevelFlowControlLimit controls the max flow control limit allocated for a QUIC connection. This controls how much data is the
// receiver willing to buffer. Once the limit is reached, the sender will send a DATA_BLOCKED frame to indicate it has more data to write,
// but it's blocked by flow control
QuicConnLevelFlowControlLimit = "quic-connection-level-flow-control-limit"
// QuicStreamLevelFlowControlLimit is similar to quicConnLevelFlowControlLimit but for each QUIC stream. When the sender is blocked,
// it will send a STREAM_DATA_BLOCKED frame
QuicStreamLevelFlowControlLimit = "quic-stream-level-flow-control-limit"
// Ui is to enable launching cloudflared in interactive UI mode
Ui = "ui"
// ConnectorLabel is the command line flag to give a meaningful label to a specific connector
ConnectorLabel = "label"
// MaxActiveFlows is the command line flag to set the maximum number of flows that cloudflared can be processing at the same time
MaxActiveFlows = "max-active-flows"
// Tag is the command line flag to set custom tags used to identify this tunnel via added HTTP request headers to the origin
Tag = "tag"
// Protocol is the command line flag to set the protocol to use to connect to the Cloudflare Edge
Protocol = "protocol"
// PostQuantum is the command line flag to force the connection to Cloudflare Edge to use Post Quantum cryptography
PostQuantum = "post-quantum"
// Features is the command line flag to opt into various features that are still being developed or tested
Features = "features"
// EdgeIpVersion is the command line flag to set the Cloudflare Edge IP address version to connect with
EdgeIpVersion = "edge-ip-version"
// EdgeBindAddress is the command line flag to bind to IP address for outgoing connections to Cloudflare Edge
EdgeBindAddress = "edge-bind-address"
// Force is the command line flag to specify if you wish to force an action
Force = "force"
// Edge is the command line flag to set the address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment
Edge = "edge"
// Region is the command line flag to set the Cloudflare Edge region to connect to
Region = "region"
// IsAutoUpdated is the command line flag to signal the new process that cloudflared has been autoupdated
IsAutoUpdated = "is-autoupdated"
// LBPool is the command line flag to set the name of the load balancing pool to add this origin to
LBPool = "lb-pool"
// Retries is the command line flag to set the maximum number of retries for connection/protocol errors
Retries = "retries"
// MaxEdgeAddrRetries is the command line flag to set the maximum number of times to retry on edge addrs before falling back to a lower protocol
MaxEdgeAddrRetries = "max-edge-addr-retries"
// GracePeriod is the command line flag to set the maximum amount of time that cloudflared waits to shut down if it is still serving requests
GracePeriod = "grace-period"
// ICMPV4Src is the command line flag to set the source address and the interface name to send/receive ICMPv4 messages
ICMPV4Src = "icmpv4-src"
// 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"
// AutoUpdateFreq is the command line for setting the frequency that cloudflared checks for updates
AutoUpdateFreq = "autoupdate-freq"
// NoAutoUpdate is the command line flag to disable cloudflared from checking for updates
NoAutoUpdate = "no-autoupdate"
// LogLevel is the command line flag for the cloudflared logging level
LogLevel = "loglevel"
// LogLevelSSH is the command line flag for the cloudflared ssh logging level
LogLevelSSH = "log-level"
// TransportLogLevel is the command line flag for the transport logging level
TransportLogLevel = "transport-loglevel"
// LogFile is the command line flag to define the file where application logs will be stored
LogFile = "logfile"
// LogDirectory is the command line flag to define the directory where application logs will be stored.
LogDirectory = "log-directory"
// TraceOutput is the command line flag to set the name of trace output file
TraceOutput = "trace-output"
// OriginCert is the command line flag to define the path for the origin certificate used by cloudflared
OriginCert = "origincert"
// Metrics is the command line flag to define the address of the metrics server
Metrics = "metrics"
// MetricsUpdateFreq is the command line flag to define how frequently tunnel metrics are updated
MetricsUpdateFreq = "metrics-update-freq"
// ApiURL is the command line flag used to define the base URL of the API
ApiURL = "api-url"
)

View File

@ -1,14 +1,40 @@
//go:build !windows && !darwin && !linux //go:build !windows && !darwin && !linux
// +build !windows,!darwin,!linux
package main package main
import ( import (
"fmt"
"os" "os"
cli "github.com/urfave/cli/v2" cli "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
) )
func runApp(app *cli.App, graceShutdownC chan struct{}) { func runApp(app *cli.App, graceShutdownC chan struct{}) {
app.Commands = append(app.Commands, &cli.Command{
Name: "service",
Usage: "Manages the cloudflared system service (not supported on this operating system)",
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install cloudflared as a system service (not supported on this operating system)",
Action: cliutil.ConfiguredAction(installGenericService),
},
{
Name: "uninstall",
Usage: "Uninstall the cloudflared service (not supported on this operating system)",
Action: cliutil.ConfiguredAction(uninstallGenericService),
},
},
})
app.Run(os.Args) app.Run(os.Args)
} }
func installGenericService(c *cli.Context) error {
return fmt.Errorf("service installation is not supported on this operating system")
}
func uninstallGenericService(c *cli.Context) error {
return fmt.Errorf("service uninstallation is not supported on this operating system")
}

View File

@ -1,5 +1,4 @@
//go:build linux //go:build linux
// +build linux
package main package main
@ -56,7 +55,8 @@ var systemdAllTemplates = map[string]ServiceTemplate{
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService), Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
Content: `[Unit] Content: `[Unit]
Description=cloudflared Description=cloudflared
After=network.target After=network-online.target
Wants=network-online.target
[Service] [Service]
TimeoutStartSec=0 TimeoutStartSec=0
@ -73,7 +73,8 @@ WantedBy=multi-user.target
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateService), Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateService),
Content: `[Unit] Content: `[Unit]
Description=Update cloudflared Description=Update cloudflared
After=network.target After=network-online.target
Wants=network-online.target
[Service] [Service]
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code' ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
@ -370,7 +371,7 @@ func uninstallSystemd(log *zerolog.Logger) error {
// Get only the installed services // Get only the installed services
installedServices := make(map[string]ServiceTemplate) installedServices := make(map[string]ServiceTemplate)
for serviceName, serviceTemplate := range systemdAllTemplates { for serviceName, serviceTemplate := range systemdAllTemplates {
if err := runCommand("systemctl", "status", serviceName); err == nil { if err := runCommand("systemctl", "list-units", "--all", "|", "grep", serviceName); err == nil {
installedServices[serviceName] = serviceTemplate installedServices[serviceName] = serviceTemplate
} else { } else {
log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName) log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName)

View File

@ -1,5 +1,4 @@
//go:build darwin //go:build darwin
// +build darwin
package main package main
@ -7,6 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -18,7 +18,7 @@ const (
launchdIdentifier = "com.cloudflare.cloudflared" launchdIdentifier = "com.cloudflare.cloudflared"
) )
func runApp(app *cli.App, graceShutdownC chan struct{}) { func runApp(app *cli.App, _ chan struct{}) {
app.Commands = append(app.Commands, &cli.Command{ app.Commands = append(app.Commands, &cli.Command{
Name: "service", Name: "service",
Usage: "Manages the cloudflared launch agent", Usage: "Manages the cloudflared launch agent",
@ -120,7 +120,7 @@ func installLaunchd(c *cli.Context) error {
log.Info().Msg("Installing cloudflared client as an user launch agent. " + log.Info().Msg("Installing cloudflared client as an user launch agent. " +
"Note that cloudflared client will only run when the user is logged in. " + "Note that cloudflared client will only run when the user is logged in. " +
"If you want to run cloudflared client at boot, install with root permission. " + "If you want to run cloudflared client at boot, install with root permission. " +
"For more information, visit https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service") "For more information, visit https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/local-management/as-a-service/macos/")
} }
etPath, err := os.Executable() etPath, err := os.Executable()
if err != nil { if err != nil {
@ -208,3 +208,15 @@ func uninstallLaunchd(c *cli.Context) error {
} }
return err return err
} }
func userHomeDir() (string, error) {
// This returns the home dir of the executing user using OS-specific method
// for discovering the home dir. It's not recommended to call this function
// when the user has root permission as $HOME depends on what options the user
// use with sudo.
homeDir, err := homedir.Dir()
if err != nil {
return "", errors.Wrap(err, "Cannot determine home directory for the user")
}
return homeDir, nil
}

View File

@ -2,18 +2,17 @@ package main
import ( import (
"fmt" "fmt"
"math/rand" "os"
"strings" "strings"
"time" "time"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"go.uber.org/automaxprocs/maxprocs" "go.uber.org/automaxprocs/maxprocs"
"github.com/cloudflare/cloudflared/cmd/cloudflared/access" "github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns" "github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tail" "github.com/cloudflare/cloudflared/cmd/cloudflared/tail"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel" "github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
@ -49,9 +48,10 @@ var (
) )
func main() { func main() {
rand.Seed(time.Now().UnixNano()) // FIXME: TUN-8148: Disable QUIC_GO ECN due to bugs in proper detection if supported
os.Setenv("QUIC_GO_DISABLE_ECN", "1")
metrics.RegisterBuildInfo(BuildType, BuildTime, Version) metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
maxprocs.Set() _, _ = maxprocs.Set()
bInfo := cliutil.GetBuildInfo(BuildType, Version) bInfo := cliutil.GetBuildInfo(BuildType, Version)
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully. // Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
@ -87,7 +87,7 @@ func main() {
tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command... tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command...
access.Init(graceShutdownC, Version) access.Init(graceShutdownC, Version)
updater.Init(Version) updater.Init(bInfo)
tracing.Init(Version) tracing.Init(Version)
token.Init(Version) token.Init(Version)
tail.Init(bInfo) tail.Init(bInfo)
@ -106,7 +106,7 @@ func commands(version func(c *cli.Context)) []*cli.Command {
Usage: "specify if you wish to update to the latest beta version", Usage: "specify if you wish to update to the latest beta version",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "force", Name: cfdflags.Force,
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version", Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
Hidden: true, Hidden: true,
}, },
@ -130,11 +130,22 @@ To determine if an update happened in a script, check for error code 11.`,
{ {
Name: "version", Name: "version",
Action: func(c *cli.Context) (err error) { Action: func(c *cli.Context) (err error) {
if c.Bool("short") {
fmt.Println(strings.Split(c.App.Version, " ")[0])
return nil
}
version(c) version(c)
return nil return nil
}, },
Usage: versionText, Usage: versionText,
Description: versionText, Description: versionText,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "short",
Aliases: []string{"s"},
Usage: "print just the version number",
},
},
}, },
} }
cmds = append(cmds, tunnel.Commands()...) cmds = append(cmds, tunnel.Commands()...)
@ -169,18 +180,6 @@ func action(graceShutdownC chan struct{}) cli.ActionFunc {
}) })
} }
func userHomeDir() (string, error) {
// This returns the home dir of the executing user using OS-specific method
// for discovering the home dir. It's not recommended to call this function
// when the user has root permission as $HOME depends on what options the user
// use with sudo.
homeDir, err := homedir.Dir()
if err != nil {
return "", errors.Wrap(err, "Cannot determine home directory for the user")
}
return homeDir, nil
}
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match. // In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
func captureError(err error) { func captureError(err error) {
errorMessage := err.Error() errorMessage := err.Error()

View File

@ -1,13 +1,13 @@
package main package main
import ( import (
"bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path" "path/filepath"
"text/template" "text/template"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
@ -44,7 +44,7 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
return err return err
} }
if _, err = os.Stat(resolvedPath); err == nil { if _, err = os.Stat(resolvedPath); err == nil {
return fmt.Errorf(serviceAlreadyExistsWarn(resolvedPath)) return errors.New(serviceAlreadyExistsWarn(resolvedPath))
} }
var buffer bytes.Buffer var buffer bytes.Buffer
@ -57,7 +57,7 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
fileMode = st.FileMode fileMode = st.FileMode
} }
plistFolder := path.Dir(resolvedPath) plistFolder := filepath.Dir(resolvedPath)
err = os.MkdirAll(plistFolder, 0o755) err = os.MkdirAll(plistFolder, 0o755)
if err != nil { if err != nil {
return fmt.Errorf("error creating %s: %v", plistFolder, err) return fmt.Errorf("error creating %s: %v", plistFolder, err)
@ -118,49 +118,6 @@ func ensureConfigDirExists(configDir string) error {
return err return err
} }
// openFile opens the file at path. If create is set and the file exists, returns nil, true, nil
func openFile(path string, create bool) (file *os.File, exists bool, err error) {
expandedPath, err := homedir.Expand(path)
if err != nil {
return nil, false, err
}
if create {
fileInfo, err := os.Stat(expandedPath)
if err == nil && fileInfo.Size() > 0 {
return nil, true, nil
}
file, err = os.OpenFile(expandedPath, os.O_RDWR|os.O_CREATE, 0600)
} else {
file, err = os.Open(expandedPath)
}
return file, false, err
}
func copyCredential(srcCredentialPath, destCredentialPath string) error {
destFile, exists, err := openFile(destCredentialPath, true)
if err != nil {
return err
} else if exists {
// credentials already exist, do nothing
return nil
}
defer destFile.Close()
srcFile, _, err := openFile(srcCredentialPath, false)
if err != nil {
return err
}
defer srcFile.Close()
// Copy certificate
_, err = io.Copy(destFile, srcFile)
if err != nil {
return fmt.Errorf("unable to copy %s to %s: %v", srcCredentialPath, destCredentialPath, err)
}
return nil
}
func copyFile(src, dest string) error { func copyFile(src, dest string) error {
srcFile, err := os.Open(src) srcFile, err := os.Open(src)
if err != nil { if err != nil {
@ -187,36 +144,3 @@ func copyFile(src, dest string) error {
ok = true ok = true
return nil return nil
} }
func copyConfig(srcConfigPath, destConfigPath string) error {
// Copy or create config
destFile, exists, err := openFile(destConfigPath, true)
if err != nil {
return fmt.Errorf("cannot open %s with error: %s", destConfigPath, err)
} else if exists {
// config already exists, do nothing
return nil
}
defer destFile.Close()
srcFile, _, err := openFile(srcConfigPath, false)
if err != nil {
fmt.Println("Your service needs a config file that at least specifies the hostname option.")
fmt.Println("Type in a hostname now, or leave it blank and create the config file later.")
fmt.Print("Hostname: ")
reader := bufio.NewReader(os.Stdin)
input, _ := reader.ReadString('\n')
if input == "" {
return err
}
fmt.Fprintf(destFile, "hostname: %s\n", input)
} else {
defer srcFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err)
}
}
return nil
}

View File

@ -18,14 +18,12 @@ import (
"nhooyr.io/websocket" "nhooyr.io/websocket"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/credentials"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/management"
) )
var ( var buildInfo *cliutil.BuildInfo
buildInfo *cliutil.BuildInfo
)
func Init(bi *cliutil.BuildInfo) { func Init(bi *cliutil.BuildInfo) {
buildInfo = bi buildInfo = bi
@ -56,7 +54,7 @@ func managementTokenCommand(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
var tokenResponse = struct { tokenResponse := struct {
Token string `json:"token"` Token string `json:"token"`
}{Token: token} }{Token: token}
@ -119,13 +117,13 @@ func buildTailCommand(subcommands []*cli.Command) *cli.Command {
Value: "", Value: "",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: logger.LogLevelFlag, Name: cfdflags.LogLevel,
Value: "info", Value: "info",
Usage: "Application logging level {debug, info, warn, error, fatal}", Usage: "Application logging level {debug, info, warn, error, fatal}",
EnvVars: []string{"TUNNEL_LOGLEVEL"}, EnvVars: []string{"TUNNEL_LOGLEVEL"},
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: credentials.OriginCertFlag, Name: cfdflags.OriginCert,
Usage: "Path to the certificate generated for your origin when you run cloudflared login.", Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
EnvVars: []string{"TUNNEL_ORIGIN_CERT"}, EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
Value: credentials.FindDefaultOriginCertPath(), Value: credentials.FindDefaultOriginCertPath(),
@ -169,7 +167,7 @@ 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 // logger will be created to emit only against the os.Stderr as to not obstruct with normal output from
// management requests // management requests
func createLogger(c *cli.Context) *zerolog.Logger { func createLogger(c *cli.Context) *zerolog.Logger {
level, levelErr := zerolog.ParseLevel(c.String(logger.LogLevelFlag)) level, levelErr := zerolog.ParseLevel(c.String(cfdflags.LogLevel))
if levelErr != nil { if levelErr != nil {
level = zerolog.InfoLevel level = zerolog.InfoLevel
} }
@ -183,9 +181,10 @@ func createLogger(c *cli.Context) *zerolog.Logger {
// parseFilters will attempt to parse provided filters to send to with the EventStartStreaming // parseFilters will attempt to parse provided filters to send to with the EventStartStreaming
func parseFilters(c *cli.Context) (*management.StreamingFilters, error) { func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
var level *management.LogLevel var level *management.LogLevel
var events []management.LogEventType
var sample float64 var sample float64
events := make([]management.LogEventType, 0)
argLevel := c.String("level") argLevel := c.String("level")
argEvents := c.StringSlice("event") argEvents := c.StringSlice("event")
argSample := c.Float64("sample") argSample := c.Float64("sample")
@ -225,12 +224,12 @@ func parseFilters(c *cli.Context) (*management.StreamingFilters, error) {
// getManagementToken will make a call to the Cloudflare API to acquire a management token for the requested tunnel. // 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) { func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) {
userCreds, err := credentials.Read(c.String(credentials.OriginCertFlag), log) userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
if err != nil { if err != nil {
return "", err return "", err
} }
client, err := userCreds.Client(c.String("api-url"), buildInfo.UserAgent(), log) client, err := userCreds.Client(c.String(cfdflags.ApiURL), buildInfo.UserAgent(), log)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -331,6 +330,7 @@ func Run(c *cli.Context) error {
header["cf-trace-id"] = []string{trace} header["cf-trace-id"] = []string{trace}
} }
ctx := c.Context ctx := c.Context
// nolint: bodyclose
conn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{ conn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
HTTPHeader: header, HTTPHeader: header,
}) })

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"path/filepath"
"runtime/trace" "runtime/trace"
"strings" "strings"
"sync" "sync"
@ -15,7 +16,7 @@ import (
"github.com/facebookgo/grace/gracenet" "github.com/facebookgo/grace/gracenet"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
"github.com/google/uuid" "github.com/google/uuid"
homedir "github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -23,13 +24,14 @@ import (
"github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns" "github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/credentials"
"github.com/cloudflare/cloudflared/diagnostic"
"github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/features"
"github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/management"
@ -39,56 +41,13 @@ import (
"github.com/cloudflare/cloudflared/supervisor" "github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns" "github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstate"
"github.com/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/validation"
) )
const ( const (
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878" sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
// ha-Connections specifies how many connections to make to the edge
haConnectionsFlag = "ha-connections"
// sshPortFlag is the port on localhost the cloudflared ssh server will run on
sshPortFlag = "local-ssh-port"
// sshIdleTimeoutFlag defines the duration a SSH session can remain idle before being closed
sshIdleTimeoutFlag = "ssh-idle-timeout"
// sshMaxTimeoutFlag defines the max duration a SSH session can remain open for
sshMaxTimeoutFlag = "ssh-max-timeout"
// bucketNameFlag is the bucket name to use for the SSH log uploader
bucketNameFlag = "bucket-name"
// regionNameFlag is the AWS region name to use for the SSH log uploader
regionNameFlag = "region-name"
// secretIDFlag is the Secret id of SSH log uploader
secretIDFlag = "secret-id"
// accessKeyIDFlag is the Access key id of SSH log uploader
accessKeyIDFlag = "access-key-id"
// sessionTokenIDFlag is the Session token of SSH log uploader
sessionTokenIDFlag = "session-token"
// s3URLFlag is the S3 URL of SSH log uploader (e.g. don't use AWS s3 and use google storage bucket instead)
s3URLFlag = "s3-url-host"
// hostKeyPath is the path of the dir to save SSH host keys too
hostKeyPath = "host-key-path"
// udpUnregisterSessionTimeout is how long we wait before we stop trying to unregister a UDP session from the edge
udpUnregisterSessionTimeoutFlag = "udp-unregister-session-timeout"
// quicDisablePathMTUDiscovery sets if QUIC should not perform PTMU discovery and use a smaller (safe) packet size.
// Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.
// Note that this may result in packet drops for UDP proxying, since we expect being able to send at least 1280 bytes of inner packets.
quicDisablePathMTUDiscovery = "quic-disable-pmtu-discovery"
// uiFlag is to enable launching cloudflared in interactive UI mode
uiFlag = "ui"
LogFieldCommand = "command" LogFieldCommand = "command"
LogFieldExpandedPath = "expandedPath" LogFieldExpandedPath = "expandedPath"
LogFieldPIDPathname = "pidPathname" LogFieldPIDPathname = "pidPathname"
@ -103,7 +62,6 @@ Eg. cloudflared tunnel --url localhost:8080/.
Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes. Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes.
For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/) For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)
` `
connectorLabelFlag = "label"
) )
var ( var (
@ -113,7 +71,96 @@ var (
routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+ routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+ "most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
"any existing DNS records for this hostname.", overwriteDNSFlag) "any existing DNS records for this hostname.", overwriteDNSFlag)
deprecatedClassicTunnelErr = fmt.Errorf("Classic tunnels have been deprecated, please use Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)") errDeprecatedClassicTunnel = errors.New("Classic tunnels have been deprecated, please use Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)")
// TODO: TUN-8756 the list below denotes the flags that do not possess any kind of sensitive information
// however this approach is not maintainble in the long-term.
nonSecretFlagsList = []string{
"config",
cfdflags.AutoUpdateFreq,
cfdflags.NoAutoUpdate,
cfdflags.Metrics,
"pidfile",
"url",
"hello-world",
"socks5",
"proxy-connect-timeout",
"proxy-tls-timeout",
"proxy-tcp-keepalive",
"proxy-no-happy-eyeballs",
"proxy-keepalive-connections",
"proxy-keepalive-timeout",
"proxy-connection-timeout",
"proxy-expect-continue-timeout",
"http-host-header",
"origin-server-name",
"unix-socket",
"origin-ca-pool",
"no-tls-verify",
"no-chunked-encoding",
"http2-origin",
"management-hostname",
"service-op-ip",
"local-ssh-port",
"ssh-idle-timeout",
"ssh-max-timeout",
"bucket-name",
"region-name",
"s3-url-host",
"host-key-path",
"ssh-server",
"bastion",
"proxy-address",
"proxy-port",
cfdflags.LogLevel,
cfdflags.TransportLogLevel,
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,
cfdflags.EdgeIpVersion,
cfdflags.EdgeBindAddress,
"cacert",
"hostname",
"id",
cfdflags.LBPool,
cfdflags.ApiURL,
cfdflags.MetricsUpdateFreq,
cfdflags.Tag,
"heartbeat-interval",
"heartbeat-count",
cfdflags.MaxEdgeAddrRetries,
cfdflags.Retries,
"ha-connections",
"rpc-timeout",
"write-stream-timeout",
"quic-disable-pmtu-discovery",
"quic-connection-level-flow-control-limit",
"quic-stream-level-flow-control-limit",
cfdflags.ConnectorLabel,
cfdflags.GracePeriod,
"compression-quality",
"use-reconnect-token",
"dial-edge-timeout",
"stdin-control",
cfdflags.Name,
cfdflags.Ui,
"quick-service",
"max-fetch-size",
cfdflags.PostQuantum,
"management-diagnostics",
cfdflags.Protocol,
"overwrite-dns",
"help",
cfdflags.MaxActiveFlows,
}
) )
func Flags() []cli.Flag { func Flags() []cli.Flag {
@ -128,11 +175,13 @@ func Commands() []*cli.Command {
buildVirtualNetworkSubcommand(false), buildVirtualNetworkSubcommand(false),
buildRunCommand(), buildRunCommand(),
buildListCommand(), buildListCommand(),
buildReadyCommand(),
buildInfoCommand(), buildInfoCommand(),
buildIngressSubcommand(), buildIngressSubcommand(),
buildDeleteCommand(), buildDeleteCommand(),
buildCleanupCommand(), buildCleanupCommand(),
buildTokenCommand(), buildTokenCommand(),
buildDiagCommand(),
// for compatibility, allow following as tunnel subcommands // for compatibility, allow following as tunnel subcommands
proxydns.Command(true), proxydns.Command(true),
cliutil.RemovedCommand("db-connect"), cliutil.RemovedCommand("db-connect"),
@ -159,7 +208,7 @@ then protect with Cloudflare Access).
B) Locally reachable TCP/UDP-based private services to Cloudflare connected private users in the same account, e.g., B) Locally reachable TCP/UDP-based private services to Cloudflare connected private users in the same account, e.g.,
those enrolled to a Zero Trust WARP Client. those enrolled to a Zero Trust WARP Client.
You can manage your Tunnels via dash.teams.cloudflare.com. This approach will only require you to run a single command You can manage your Tunnels via one.dash.cloudflare.com. This approach will only require you to run a single command
later in each machine where you wish to run a Tunnel. later in each machine where you wish to run a Tunnel.
Alternatively, you can manage your Tunnels via the command line. Begin by obtaining a certificate to be able to do so: Alternatively, you can manage your Tunnels via the command line. Begin by obtaining a certificate to be able to do so:
@ -195,7 +244,7 @@ func TunnelCommand(c *cli.Context) error {
// --name required // --name required
// --url or --hello-world required // --url or --hello-world required
// --hostname optional // --hostname optional
if name := c.String("name"); name != "" { if name := c.String(cfdflags.Name); name != "" {
hostname, err := validation.ValidateHostname(c.String("hostname")) hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil { if err != nil {
return errors.Wrap(err, "Invalid hostname provided") return errors.Wrap(err, "Invalid hostname provided")
@ -212,7 +261,7 @@ func TunnelCommand(c *cli.Context) error {
// A unauthenticated named tunnel hosted on <random>.<quick-tunnels-service>.com // 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 // 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) shouldRunQuickTunnel := c.IsSet("url") || c.IsSet(ingress.HelloWorldFlag)
if !c.IsSet("proxy-dns") && c.String("quick-service") != "" && shouldRunQuickTunnel { if !c.IsSet(cfdflags.ProxyDns) && c.String("quick-service") != "" && shouldRunQuickTunnel {
return RunQuickTunnel(sc) return RunQuickTunnel(sc)
} }
@ -223,10 +272,10 @@ func TunnelCommand(c *cli.Context) error {
// Classic tunnel usage is no longer supported // Classic tunnel usage is no longer supported
if c.String("hostname") != "" { if c.String("hostname") != "" {
return deprecatedClassicTunnelErr return errDeprecatedClassicTunnel
} }
if c.IsSet("proxy-dns") { if c.IsSet(cfdflags.ProxyDns) {
if shouldRunQuickTunnel { if shouldRunQuickTunnel {
return fmt.Errorf("running a quick tunnel with `proxy-dns` is not supported") return fmt.Errorf("running a quick tunnel with `proxy-dns` is not supported")
} }
@ -273,7 +322,7 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri
func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) { func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
if hostname := c.String("hostname"); hostname != "" { if hostname := c.String("hostname"); hostname != "" {
if lbPool := c.String("lb-pool"); lbPool != "" { if lbPool := c.String(cfdflags.LBPool); lbPool != "" {
return cfapi.NewLBRoute(hostname, lbPool), true return cfapi.NewLBRoute(hostname, lbPool), true
} }
return cfapi.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true return cfapi.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
@ -284,7 +333,7 @@ func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
func StartServer( func StartServer(
c *cli.Context, c *cli.Context,
info *cliutil.BuildInfo, info *cliutil.BuildInfo,
namedTunnel *connection.NamedTunnelProperties, namedTunnel *connection.TunnelProperties,
log *zerolog.Logger, log *zerolog.Logger,
) error { ) error {
err := sentry.Init(sentry.ClientOptions{ err := sentry.Init(sentry.ClientOptions{
@ -303,7 +352,7 @@ func StartServer(
log.Info().Msg(config.ErrNoConfigFile.Error()) log.Info().Msg(config.ErrNoConfigFile.Error())
} }
if c.IsSet("trace-output") { if c.IsSet(cfdflags.TraceOutput) {
tmpTraceFile, err := os.CreateTemp("", "trace") tmpTraceFile, err := os.CreateTemp("", "trace")
if err != nil { if err != nil {
log.Err(err).Msg("Failed to create new temporary file to save trace output") log.Err(err).Msg("Failed to create new temporary file to save trace output")
@ -315,7 +364,7 @@ func StartServer(
if err := tmpTraceFile.Close(); err != nil { if err := tmpTraceFile.Close(); err != nil {
traceLog.Err(err).Msg("Failed to close temporary trace output file") traceLog.Err(err).Msg("Failed to close temporary trace output file")
} }
traceOutputFilepath := c.String("trace-output") traceOutputFilepath := c.String(cfdflags.TraceOutput)
if err := os.Rename(tmpTraceFile.Name(), traceOutputFilepath); err != nil { if err := os.Rename(tmpTraceFile.Name(), traceOutputFilepath); err != nil {
traceLog. traceLog.
Err(err). Err(err).
@ -340,12 +389,12 @@ func StartServer(
logClientOptions(c, 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 cancelled tunnel and all other components (origins, dns, etc...) should stop
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(c.Context)
defer cancel() defer cancel()
go waitForSignal(graceShutdownC, log) go waitForSignal(graceShutdownC, log)
if c.IsSet("proxy-dns") { if c.IsSet(cfdflags.ProxyDns) {
dnsReadySignal := make(chan struct{}) dnsReadySignal := make(chan struct{})
wg.Add(1) wg.Add(1)
go func() { go func() {
@ -367,7 +416,7 @@ func StartServer(
go func() { go func() {
defer wg.Done() defer wg.Done()
autoupdater := updater.NewAutoUpdater( autoupdater := updater.NewAutoUpdater(
c.Bool("no-autoupdate"), c.Duration("autoupdate-freq"), &listeners, log, c.Bool(cfdflags.NoAutoUpdate), c.Duration(cfdflags.AutoUpdateFreq), &listeners, log,
) )
errC <- autoupdater.Run(ctx) errC <- autoupdater.Run(ctx)
}() }()
@ -392,7 +441,7 @@ func StartServer(
observer.SendURL(quickTunnelURL) observer.SendURL(quickTunnelURL)
} }
tunnelConfig, orchestratorConfig, err := prepareTunnelConfig(c, info, log, logTransport, observer, namedTunnel) tunnelConfig, orchestratorConfig, err := prepareTunnelConfig(ctx, c, info, log, logTransport, observer, namedTunnel)
if err != nil { if err != nil {
log.Err(err).Msg("Couldn't start tunnel") log.Err(err).Msg("Couldn't start tunnel")
return err return err
@ -406,51 +455,76 @@ func StartServer(
} }
} }
internalRules := []ingress.Rule{} // Disable ICMP packet routing for quick tunnels
if features.Contains(features.FeatureManagementLogs) { if quickTunnelURL != "" {
serviceIP := c.String("service-op-ip") tunnelConfig.ICMPRouterServer = nil
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
if serviceAddr, err := edgeAddrs.GetAddrForRPC(); err == nil {
serviceIP = serviceAddr.TCP.String()
}
}
mgmt := management.New(
c.String("management-hostname"),
c.Bool("management-diagnostics"),
serviceIP,
clientID,
c.String(connectorLabelFlag),
logger.ManagementLogger.Log,
logger.ManagementLogger,
)
internalRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
} }
serviceIP := c.String("service-op-ip")
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
if serviceAddr, err := edgeAddrs.GetAddrForRPC(); err == nil {
serviceIP = serviceAddr.TCP.String()
}
}
mgmt := management.New(
c.String("management-hostname"),
c.Bool("management-diagnostics"),
serviceIP,
clientID,
c.String(cfdflags.ConnectorLabel),
logger.ManagementLogger.Log,
logger.ManagementLogger,
)
internalRules := []ingress.Rule{ingress.NewManagementRule(mgmt)}
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log) orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log)
if err != nil { if err != nil {
return err return err
} }
metricsListener, err := listeners.Listen("tcp", c.String("metrics")) metricsListener, err := metrics.CreateMetricsListener(&listeners, c.String("metrics"))
if err != nil { if err != nil {
log.Err(err).Msg("Error opening metrics server listener") log.Err(err).Msg("Error opening metrics server listener")
return errors.Wrap(err, "Error opening metrics server listener") return errors.Wrap(err, "Error opening metrics server listener")
} }
defer metricsListener.Close() defer metricsListener.Close()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
readinessServer := metrics.NewReadyServer(log, clientID) tracker := tunnelstate.NewConnTracker(log)
observer.RegisterSink(readinessServer) observer.RegisterSink(tracker)
ipv4, ipv6, err := determineICMPSources(c, log)
sources := make([]string, 0)
if err == nil {
sources = append(sources, ipv4.String())
sources = append(sources, ipv6.String())
}
readinessServer := metrics.NewReadyServer(clientID, tracker)
cliFlags := nonSecretCliFlags(log, c, nonSecretFlagsList)
diagnosticHandler := diagnostic.NewDiagnosticHandler(
log,
0,
diagnostic.NewSystemCollectorImpl(buildInfo.CloudflaredVersion),
tunnelConfig.NamedTunnel.Credentials.TunnelID,
clientID,
tracker,
cliFlags,
sources,
)
metricsConfig := metrics.Config{ metricsConfig := metrics.Config{
ReadyServer: readinessServer, ReadyServer: readinessServer,
DiagnosticHandler: diagnosticHandler,
QuickTunnelHostname: quickTunnelURL, QuickTunnelHostname: quickTunnelURL,
Orchestrator: orchestrator, Orchestrator: orchestrator,
} }
errC <- metrics.ServeMetrics(metricsListener, ctx, metricsConfig, log) errC <- metrics.ServeMetrics(metricsListener, ctx, metricsConfig, log)
}() }()
reconnectCh := make(chan supervisor.ReconnectSignal, c.Int(haConnectionsFlag)) reconnectCh := make(chan supervisor.ReconnectSignal, c.Int(cfdflags.HaConnections))
if c.IsSet("stdin-control") { if c.IsSet("stdin-control") {
log.Info().Msg("Enabling control through stdin") log.Info().Msg("Enabling control through stdin")
go stdinControl(reconnectCh, log) go stdinControl(reconnectCh, log)
@ -487,8 +561,10 @@ func waitToShutdown(wg *sync.WaitGroup,
log.Debug().Msg("Graceful shutdown signalled") log.Debug().Msg("Graceful shutdown signalled")
if gracePeriod > 0 { if gracePeriod > 0 {
// wait for either grace period or service termination // wait for either grace period or service termination
ticker := time.NewTicker(gracePeriod)
defer ticker.Stop()
select { select {
case <-time.Tick(gracePeriod): case <-ticker.C:
case <-errC: case <-errC:
} }
} }
@ -516,7 +592,7 @@ func waitToShutdown(wg *sync.WaitGroup,
func notifySystemd(waitForSignal *signal.Signal) { func notifySystemd(waitForSignal *signal.Signal) {
<-waitForSignal.Wait() <-waitForSignal.Wait()
daemon.SdNotify(false, "READY=1") _, _ = daemon.SdNotify(false, "READY=1")
} }
func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog.Logger) { func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog.Logger) {
@ -568,31 +644,31 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
flags = append(flags, []cli.Flag{ flags = append(flags, []cli.Flag{
credentialsFileFlag, credentialsFileFlag,
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "is-autoupdated", Name: cfdflags.IsAutoUpdated,
Usage: "Signal the new process that Cloudflare Tunnel connector has been autoupdated", Usage: "Signal the new process that Cloudflare Tunnel connector has been autoupdated",
Value: false, Value: false,
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "edge", Name: cfdflags.Edge,
Usage: "Address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment.", Usage: "Address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment.",
EnvVars: []string{"TUNNEL_EDGE"}, EnvVars: []string{"TUNNEL_EDGE"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "region", Name: cfdflags.Region,
Usage: "Cloudflare Edge region to connect to. Omit or set to empty to connect to the global region.", Usage: "Cloudflare Edge region to connect to. Omit or set to empty to connect to the global region.",
EnvVars: []string{"TUNNEL_REGION"}, EnvVars: []string{"TUNNEL_REGION"},
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "edge-ip-version", Name: cfdflags.EdgeIpVersion,
Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}", Usage: "Cloudflare Edge IP address version to connect with. {4, 6, auto}",
EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"}, EnvVars: []string{"TUNNEL_EDGE_IP_VERSION"},
Value: "4", Value: "4",
Hidden: false, Hidden: false,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "edge-bind-address", Name: cfdflags.EdgeBindAddress,
Usage: "Bind to IP address for outgoing connections to Cloudflare Edge.", Usage: "Bind to IP address for outgoing connections to Cloudflare Edge.",
EnvVars: []string{"TUNNEL_EDGE_BIND_ADDRESS"}, EnvVars: []string{"TUNNEL_EDGE_BIND_ADDRESS"},
Hidden: false, Hidden: false,
@ -616,7 +692,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "lb-pool", Name: cfdflags.LBPool,
Usage: "The name of a (new/existing) load balancing pool to add this origin to.", Usage: "The name of a (new/existing) load balancing pool to add this origin to.",
EnvVars: []string{"TUNNEL_LB_POOL"}, EnvVars: []string{"TUNNEL_LB_POOL"},
Hidden: shouldHide, Hidden: shouldHide,
@ -640,24 +716,24 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-url", Name: cfdflags.ApiURL,
Usage: "Base URL for Cloudflare API v4", Usage: "Base URL for Cloudflare API v4",
EnvVars: []string{"TUNNEL_API_URL"}, EnvVars: []string{"TUNNEL_API_URL"},
Value: "https://api.cloudflare.com/client/v4", Value: "https://api.cloudflare.com/client/v4",
Hidden: true, Hidden: true,
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "metrics-update-freq", Name: cfdflags.MetricsUpdateFreq,
Usage: "Frequency to update tunnel metrics", Usage: "Frequency to update tunnel metrics",
Value: time.Second * 5, Value: time.Second * 5,
EnvVars: []string{"TUNNEL_METRICS_UPDATE_FREQ"}, EnvVars: []string{"TUNNEL_METRICS_UPDATE_FREQ"},
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "tag", Name: cfdflags.Tag,
Usage: "Custom tags used to identify this tunnel, in format `KEY=VALUE`. Multiple tags may be specified", Usage: "Custom tags used to identify this tunnel via added HTTP request headers to the origin, in format `KEY=VALUE`. Multiple tags may be specified.",
EnvVars: []string{"TUNNEL_TAG"}, EnvVars: []string{"TUNNEL_TAG"},
Hidden: shouldHide, Hidden: true,
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "heartbeat-interval", Name: "heartbeat-interval",
@ -673,43 +749,64 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: true, Hidden: true,
}), }),
altsrc.NewIntFlag(&cli.IntFlag{ altsrc.NewIntFlag(&cli.IntFlag{
Name: "max-edge-addr-retries", Name: cfdflags.MaxEdgeAddrRetries,
Usage: "Maximum number of times to retry on edge addrs before falling back to a lower protocol", Usage: "Maximum number of times to retry on edge addrs before falling back to a lower protocol",
Value: 8, Value: 8,
Hidden: true, Hidden: true,
}), }),
// Note TUN-3758 , we use Int because UInt is not supported with altsrc // Note TUN-3758 , we use Int because UInt is not supported with altsrc
altsrc.NewIntFlag(&cli.IntFlag{ altsrc.NewIntFlag(&cli.IntFlag{
Name: "retries", Name: cfdflags.Retries,
Value: 5, Value: 5,
Usage: "Maximum number of retries for connection/protocol errors.", Usage: "Maximum number of retries for connection/protocol errors.",
EnvVars: []string{"TUNNEL_RETRIES"}, EnvVars: []string{"TUNNEL_RETRIES"},
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewIntFlag(&cli.IntFlag{ altsrc.NewIntFlag(&cli.IntFlag{
Name: haConnectionsFlag, Name: cfdflags.HaConnections,
Value: 4, Value: 4,
Hidden: true, Hidden: true,
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: udpUnregisterSessionTimeoutFlag, Name: cfdflags.RpcTimeout,
Value: 5 * time.Second, Value: 5 * time.Second,
Hidden: true, Hidden: true,
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: cfdflags.WriteStreamTimeout,
EnvVars: []string{"TUNNEL_STREAM_WRITE_TIMEOUT"},
Usage: "Use this option to add a stream write timeout for connections when writing towards the origin or edge. Default is 0 which disables the write timeout.",
Value: 0 * time.Second,
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: quicDisablePathMTUDiscovery, Name: cfdflags.QuicDisablePathMTUDiscovery,
EnvVars: []string{"TUNNEL_DISABLE_QUIC_PMTU"}, EnvVars: []string{"TUNNEL_DISABLE_QUIC_PMTU"},
Usage: "Use this option to disable PTMU discovery for QUIC connections. This will result in lower packet sizes. Not however, that this may cause instability for UDP proxying.", Usage: "Use this option to disable PTMU discovery for QUIC connections. This will result in lower packet sizes. Not however, that this may cause instability for UDP proxying.",
Value: false, Value: false,
Hidden: true, Hidden: true,
}), }),
altsrc.NewIntFlag(&cli.IntFlag{
Name: cfdflags.QuicConnLevelFlowControlLimit,
EnvVars: []string{"TUNNEL_QUIC_CONN_LEVEL_FLOW_CONTROL_LIMIT"},
Usage: "Use this option to change the connection-level flow control limit for QUIC transport.",
Value: 30 * (1 << 20), // 30 MB
Hidden: true,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: cfdflags.QuicStreamLevelFlowControlLimit,
EnvVars: []string{"TUNNEL_QUIC_STREAM_LEVEL_FLOW_CONTROL_LIMIT"},
Usage: "Use this option to change the connection-level flow control limit for QUIC transport.",
Value: 6 * (1 << 20), // 6 MB
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: connectorLabelFlag, Name: cfdflags.ConnectorLabel,
Usage: "Use this option to give a meaningful label to a specific connector. When a tunnel starts up, a connector id unique to the tunnel is generated. This is a uuid. To make it easier to identify a connector, we will use the hostname of the machine the tunnel is running on along with the connector ID. This option exists if one wants to have more control over what their individual connectors are called.", Usage: "Use this option to give a meaningful label to a specific connector. When a tunnel starts up, a connector id unique to the tunnel is generated. This is a uuid. To make it easier to identify a connector, we will use the hostname of the machine the tunnel is running on along with the connector ID. This option exists if one wants to have more control over what their individual connectors are called.",
Value: "", Value: "",
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "grace-period", Name: cfdflags.GracePeriod,
Usage: "When cloudflared receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shutdown. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received.", Usage: "When cloudflared receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shutdown. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received.",
Value: time.Second * 30, Value: time.Second * 30,
EnvVars: []string{"TUNNEL_GRACE_PERIOD"}, EnvVars: []string{"TUNNEL_GRACE_PERIOD"},
@ -745,14 +842,14 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Value: false, Value: false,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "name", Name: cfdflags.Name,
Aliases: []string{"n"}, Aliases: []string{"n"},
EnvVars: []string{"TUNNEL_NAME"}, EnvVars: []string{"TUNNEL_NAME"},
Usage: "Stable name to identify the tunnel. Using this flag will create, route and run a tunnel. For production usage, execute each command separately", Usage: "Stable name to identify the tunnel. Using this flag will create, route and run a tunnel. For production usage, execute each command separately",
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: uiFlag, Name: cfdflags.Ui,
Usage: "(depreciated) Launch tunnel UI. Tunnel logs are scrollable via 'j', 'k', or arrow keys.", Usage: "(depreciated) Launch tunnel UI. Tunnel logs are scrollable via 'j', 'k', or arrow keys.",
Value: false, Value: false,
Hidden: true, Hidden: true,
@ -770,17 +867,16 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: true, Hidden: true,
}), }),
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "post-quantum", Name: cfdflags.PostQuantum,
Usage: "When given creates an experimental post-quantum secure tunnel", Usage: "When given creates an experimental post-quantum secure tunnel",
Aliases: []string{"pq"}, Aliases: []string{"pq"},
EnvVars: []string{"TUNNEL_POST_QUANTUM"}, EnvVars: []string{"TUNNEL_POST_QUANTUM"},
Hidden: FipsEnabled,
}), }),
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "management-diagnostics", Name: "management-diagnostics",
Usage: "Enables the in-depth diagnostic routes to be made available over the management service (/debug/pprof, /metrics, etc.)", Usage: "Enables the in-depth diagnostic routes to be made available over the management service (/debug/pprof, /metrics, etc.)",
EnvVars: []string{"TUNNEL_MANAGEMENT_DIAGNOSTICS"}, EnvVars: []string{"TUNNEL_MANAGEMENT_DIAGNOSTICS"},
Value: false, Value: true,
}), }),
selectProtocolFlag, selectProtocolFlag,
overwriteDNSFlag, overwriteDNSFlag,
@ -799,29 +895,35 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide, Hidden: shouldHide,
}, },
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: credentials.OriginCertFlag, Name: cfdflags.OriginCert,
Usage: "Path to the certificate generated for your origin when you run cloudflared login.", Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
EnvVars: []string{"TUNNEL_ORIGIN_CERT"}, EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
Value: credentials.FindDefaultOriginCertPath(), Value: credentials.FindDefaultOriginCertPath(),
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "autoupdate-freq", Name: cfdflags.AutoUpdateFreq,
Usage: fmt.Sprintf("Autoupdate frequency. Default is %v.", updater.DefaultCheckUpdateFreq), Usage: fmt.Sprintf("Autoupdate frequency. Default is %v.", updater.DefaultCheckUpdateFreq),
Value: updater.DefaultCheckUpdateFreq, Value: updater.DefaultCheckUpdateFreq,
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-autoupdate", Name: cfdflags.NoAutoUpdate,
Usage: "Disable periodic check for updates, restarting the server with the new version.", Usage: "Disable periodic check for updates, restarting the server with the new version.",
EnvVars: []string{"NO_AUTOUPDATE"}, EnvVars: []string{"NO_AUTOUPDATE"},
Value: false, Value: false,
Hidden: shouldHide, Hidden: shouldHide,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: "metrics", Name: cfdflags.Metrics,
Value: "localhost:", Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
Usage: "Listen address for metrics reporting.", Usage: fmt.Sprintf(
`Listen address for metrics reporting. If no address is passed cloudflared will try to bind to %v.
If all are unavailable, a random port will be used. Note that when running cloudflared from an virtual
environment the default address binds to all interfaces, hence, it is important to isolate the host
and virtualized host network stacks from each other`,
metrics.GetMetricsKnownAddresses(metrics.Runtime),
),
EnvVars: []string{"TUNNEL_METRICS"}, EnvVars: []string{"TUNNEL_METRICS"},
Hidden: shouldHide, Hidden: shouldHide,
}), }),
@ -977,62 +1079,62 @@ func legacyTunnelFlag(msg string) string {
func sshFlags(shouldHide bool) []cli.Flag { func sshFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{ return []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: sshPortFlag, Name: cfdflags.SshPort,
Usage: "Localhost port that cloudflared SSH server will run on", Usage: "Localhost port that cloudflared SSH server will run on",
Value: "2222", Value: "2222",
EnvVars: []string{"LOCAL_SSH_PORT"}, EnvVars: []string{"LOCAL_SSH_PORT"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: sshIdleTimeoutFlag, Name: cfdflags.SshIdleTimeout,
Usage: "Connection timeout after no activity", Usage: "Connection timeout after no activity",
EnvVars: []string{"SSH_IDLE_TIMEOUT"}, EnvVars: []string{"SSH_IDLE_TIMEOUT"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: sshMaxTimeoutFlag, Name: cfdflags.SshMaxTimeout,
Usage: "Absolute connection timeout", Usage: "Absolute connection timeout",
EnvVars: []string{"SSH_MAX_TIMEOUT"}, EnvVars: []string{"SSH_MAX_TIMEOUT"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: bucketNameFlag, Name: cfdflags.SshLogUploaderBucketName,
Usage: "Bucket name of where to upload SSH logs", Usage: "Bucket name of where to upload SSH logs",
EnvVars: []string{"BUCKET_ID"}, EnvVars: []string{"BUCKET_ID"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: regionNameFlag, Name: cfdflags.SshLogUploaderRegionName,
Usage: "Region name of where to upload SSH logs", Usage: "Region name of where to upload SSH logs",
EnvVars: []string{"REGION_ID"}, EnvVars: []string{"REGION_ID"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: secretIDFlag, Name: cfdflags.SshLogUploaderSecretID,
Usage: "Secret ID of where to upload SSH logs", Usage: "Secret ID of where to upload SSH logs",
EnvVars: []string{"SECRET_ID"}, EnvVars: []string{"SECRET_ID"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: accessKeyIDFlag, Name: cfdflags.SshLogUploaderAccessKeyID,
Usage: "Access Key ID of where to upload SSH logs", Usage: "Access Key ID of where to upload SSH logs",
EnvVars: []string{"ACCESS_CLIENT_ID"}, EnvVars: []string{"ACCESS_CLIENT_ID"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: sessionTokenIDFlag, Name: cfdflags.SshLogUploaderSessionTokenID,
Usage: "Session Token to use in the configuration of SSH logs uploading", Usage: "Session Token to use in the configuration of SSH logs uploading",
EnvVars: []string{"SESSION_TOKEN_ID"}, EnvVars: []string{"SESSION_TOKEN_ID"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{ altsrc.NewStringFlag(&cli.StringFlag{
Name: s3URLFlag, Name: cfdflags.SshLogUploaderS3URL,
Usage: "S3 url of where to upload SSH logs", Usage: "S3 url of where to upload SSH logs",
EnvVars: []string{"S3_URL"}, EnvVars: []string{"S3_URL"},
Hidden: true, Hidden: true,
}), }),
altsrc.NewPathFlag(&cli.PathFlag{ altsrc.NewPathFlag(&cli.PathFlag{
Name: hostKeyPath, Name: cfdflags.HostKeyPath,
Usage: "Absolute path of directory to save SSH host keys in", Usage: "Absolute path of directory to save SSH host keys in",
EnvVars: []string{"HOST_KEY_PATH"}, EnvVars: []string{"HOST_KEY_PATH"},
Hidden: true, Hidden: true,
@ -1072,7 +1174,7 @@ func sshFlags(shouldHide bool) []cli.Flag {
func configureProxyDNSFlags(shouldHide bool) []cli.Flag { func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{ return []cli.Flag{
altsrc.NewBoolFlag(&cli.BoolFlag{ altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-dns", Name: cfdflags.ProxyDns,
Usage: "Run a DNS over HTTPS proxy server.", Usage: "Run a DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS"}, EnvVars: []string{"TUNNEL_DNS"},
Hidden: shouldHide, Hidden: shouldHide,
@ -1152,3 +1254,46 @@ reconnect [delay]
} }
} }
} }
func nonSecretCliFlags(log *zerolog.Logger, cli *cli.Context, flagInclusionList []string) map[string]string {
flagsNames := cli.FlagNames()
flags := make(map[string]string, len(flagsNames))
for _, flag := range flagsNames {
value := cli.String(flag)
if value == "" {
continue
}
isIncluded := isFlagIncluded(flagInclusionList, flag)
if !isIncluded {
continue
}
switch flag {
case cfdflags.LogDirectory, cfdflags.LogFile:
{
absolute, err := filepath.Abs(value)
if err != nil {
log.Error().Err(err).Msgf("could not convert %s path to absolute", flag)
} else {
flags[flag] = absolute
}
}
default:
flags[flag] = value
}
}
return flags
}
func isFlagIncluded(flagInclusionList []string, flag string) bool {
for _, include := range flagInclusionList {
if include == flag {
return true
}
}
return false
}

View File

@ -1,13 +0,0 @@
package tunnel
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestDedup(t *testing.T) {
expected := []string{"a", "b"}
actual := dedup([]string{"a", "b", "a"})
require.ElementsMatch(t, expected, actual)
}

View File

@ -1,9 +1,9 @@
package tunnel package tunnel
import ( import (
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
mathRand "math/rand"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@ -18,6 +18,7 @@ import (
"golang.org/x/term" "golang.org/x/term"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery"
@ -27,29 +28,33 @@ import (
"github.com/cloudflare/cloudflared/orchestration" "github.com/cloudflare/cloudflared/orchestration"
"github.com/cloudflare/cloudflared/supervisor" "github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
const secretValue = "*****" const (
secretValue = "*****"
icmpFunnelTimeout = time.Second * 10
fedRampRegion = "fed" // const string denoting the region used to connect to FEDRamp servers
)
var ( var (
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
serviceUrl = developerPortal + "/reference/service/"
argumentsUrl = developerPortal + "/reference/arguments/"
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag} secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
configFlags = []string{"autoupdate-freq", "no-autoupdate", "retries", "protocol", "loglevel", "transport-loglevel", "origincert", "metrics", "metrics-update-freq", "edge-ip-version", "edge-bind-address"} configFlags = []string{
) flags.AutoUpdateFreq,
flags.NoAutoUpdate,
func generateRandomClientID(log *zerolog.Logger) (string, error) { flags.Retries,
u, err := uuid.NewRandom() flags.Protocol,
if err != nil { flags.LogLevel,
log.Error().Msgf("couldn't create UUID for client ID %s", err) flags.TransportLogLevel,
return "", err flags.OriginCert,
flags.Metrics,
flags.MetricsUpdateFreq,
flags.EdgeIpVersion,
flags.EdgeBindAddress,
flags.MaxActiveFlows,
} }
return u.String(), nil )
}
func logClientOptions(c *cli.Context, log *zerolog.Logger) { func logClientOptions(c *cli.Context, log *zerolog.Logger) {
flags := make(map[string]interface{}) flags := make(map[string]interface{})
@ -105,38 +110,43 @@ func isSecretEnvVar(key string) bool {
return false return false
} }
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelProperties) bool { func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.TunnelProperties) bool {
return c.IsSet("proxy-dns") && return c.IsSet(flags.ProxyDns) &&
!(c.IsSet("name") || // adhoc-named tunnel !(c.IsSet(flags.Name) || // adhoc-named tunnel
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
namedTunnel != nil) // named tunnel namedTunnel != nil) // named tunnel
} }
func prepareTunnelConfig( func prepareTunnelConfig(
ctx context.Context,
c *cli.Context, c *cli.Context,
info *cliutil.BuildInfo, info *cliutil.BuildInfo,
log, logTransport *zerolog.Logger, log, logTransport *zerolog.Logger,
observer *connection.Observer, observer *connection.Observer,
namedTunnel *connection.NamedTunnelProperties, namedTunnel *connection.TunnelProperties,
) (*supervisor.TunnelConfig, *orchestration.Config, error) { ) (*supervisor.TunnelConfig, *orchestration.Config, error) {
clientID, err := uuid.NewRandom() clientID, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "can't generate connector UUID") return nil, nil, errors.Wrap(err, "can't generate connector UUID")
} }
log.Info().Msgf("Generated Connector ID: %s", clientID) log.Info().Msgf("Generated Connector ID: %s", clientID)
tags, err := NewTagSliceFromCLI(c.StringSlice("tag")) tags, err := NewTagSliceFromCLI(c.StringSlice(flags.Tag))
if err != nil { if err != nil {
log.Err(err).Msg("Tag parse failure") log.Err(err).Msg("Tag parse failure")
return nil, nil, errors.Wrap(err, "Tag parse failure") return nil, nil, errors.Wrap(err, "Tag parse failure")
} }
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID.String()}) tags = append(tags, pogs.Tag{Name: "ID", Value: clientID.String()})
transportProtocol := c.String("protocol") transportProtocol := c.String(flags.Protocol)
needPQ := c.Bool("post-quantum") isPostQuantumEnforced := c.Bool(flags.PostQuantum)
if needPQ {
if FipsEnabled { featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, c.StringSlice(flags.Features), c.Bool(flags.PostQuantum), log)
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode") if err != nil {
} return nil, nil, errors.Wrap(err, "Failed to create feature selector")
}
clientFeatures := featureSelector.ClientFeatures()
pqMode := featureSelector.PostQuantumMode()
if pqMode == features.PostQuantumStrict {
// Error if the user tries to force a non-quic transport protocol // Error if the user tries to force a non-quic transport protocol
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() { if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport") return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
@ -144,11 +154,7 @@ func prepareTunnelConfig(
transportProtocol = connection.QUIC.String() transportProtocol = connection.QUIC.String()
} }
clientFeatures := dedup(append(c.StringSlice("features"), features.DefaultFeatures...)) namedTunnel.Client = pogs.ClientInfo{
if needPQ {
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
}
namedTunnel.Client = tunnelpogs.ClientInfo{
ClientID: clientID[:], ClientID: clientID[:],
Features: clientFeatures, Features: clientFeatures,
Version: info.Version(), Version: info.Version(),
@ -160,7 +166,7 @@ func prepareTunnelConfig(
return nil, nil, err return nil, nil, err
} }
protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), c.Bool("post-quantum"), edgediscovery.ProtocolPercentage, connection.ResolveTTL, log) protocolSelector, err := connection.NewProtocolSelector(transportProtocol, namedTunnel.Credentials.AccountTag, c.IsSet(TunnelTokenFlag), isPostQuantumEnforced, edgediscovery.ProtocolPercentage, connection.ResolveTTL, log)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -186,11 +192,11 @@ func prepareTunnelConfig(
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
edgeIPVersion, err := parseConfigIPVersion(c.String("edge-ip-version")) edgeIPVersion, err := parseConfigIPVersion(c.String(flags.EdgeIpVersion))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
edgeBindAddr, err := parseConfigBindAddress(c.String("edge-bind-address")) edgeBindAddr, err := parseConfigBindAddress(c.String(flags.EdgeBindAddress))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -203,55 +209,62 @@ func prepareTunnelConfig(
log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version") log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version")
} }
var pqKexIdx int region := c.String(flags.Region)
if needPQ { endpoint := namedTunnel.Credentials.Endpoint
pqKexIdx = mathRand.Intn(len(supervisor.PQKexes)) var resolvedRegion string
log.Info().Msgf( // set resolvedRegion to either the region passed as argument
"Using experimental hybrid post-quantum key agreement %s", // or to the endpoint in the credentials.
supervisor.PQKexNames[supervisor.PQKexes[pqKexIdx]], // Region and endpoint are interchangeable
) if region != "" && endpoint != "" {
return nil, nil, fmt.Errorf("region provided with a token that has an endpoint")
} else if region != "" {
resolvedRegion = region
} else if endpoint != "" {
resolvedRegion = endpoint
} }
tunnelConfig := &supervisor.TunnelConfig{ tunnelConfig := &supervisor.TunnelConfig{
GracePeriod: gracePeriod, GracePeriod: gracePeriod,
ReplaceExisting: c.Bool("force"), ReplaceExisting: c.Bool(flags.Force),
OSArch: info.OSArch(), OSArch: info.OSArch(),
ClientID: clientID.String(), ClientID: clientID.String(),
EdgeAddrs: c.StringSlice("edge"), EdgeAddrs: c.StringSlice(flags.Edge),
Region: c.String("region"), Region: resolvedRegion,
EdgeIPVersion: edgeIPVersion, EdgeIPVersion: edgeIPVersion,
EdgeBindAddr: edgeBindAddr, EdgeBindAddr: edgeBindAddr,
HAConnections: c.Int(haConnectionsFlag), HAConnections: c.Int(flags.HaConnections),
IncidentLookup: supervisor.NewIncidentLookup(), IsAutoupdated: c.Bool(flags.IsAutoUpdated),
IsAutoupdated: c.Bool("is-autoupdated"), LBPool: c.String(flags.LBPool),
LBPool: c.String("lb-pool"),
Tags: tags, Tags: tags,
Log: log, Log: log,
LogTransport: logTransport, LogTransport: logTransport,
Observer: observer, Observer: observer,
ReportedVersion: info.Version(), ReportedVersion: info.Version(),
// Note TUN-3758 , we use Int because UInt is not supported with altsrc // Note TUN-3758 , we use Int because UInt is not supported with altsrc
Retries: uint(c.Int("retries")), Retries: uint(c.Int(flags.Retries)), // nolint: gosec
RunFromTerminal: isRunningFromTerminal(), RunFromTerminal: isRunningFromTerminal(),
NamedTunnel: namedTunnel, NamedTunnel: namedTunnel,
ProtocolSelector: protocolSelector, ProtocolSelector: protocolSelector,
EdgeTLSConfigs: edgeTLSConfigs, EdgeTLSConfigs: edgeTLSConfigs,
NeedPQ: needPQ, FeatureSelector: featureSelector,
PQKexIdx: pqKexIdx, MaxEdgeAddrRetries: uint8(c.Int(flags.MaxEdgeAddrRetries)), // nolint: gosec
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")), RPCTimeout: c.Duration(flags.RpcTimeout),
UDPUnregisterSessionTimeout: c.Duration(udpUnregisterSessionTimeoutFlag), WriteStreamTimeout: c.Duration(flags.WriteStreamTimeout),
DisableQUICPathMTUDiscovery: c.Bool(quicDisablePathMTUDiscovery), DisableQUICPathMTUDiscovery: c.Bool(flags.QuicDisablePathMTUDiscovery),
QUICConnectionLevelFlowControlLimit: c.Uint64(flags.QuicConnLevelFlowControlLimit),
QUICStreamLevelFlowControlLimit: c.Uint64(flags.QuicStreamLevelFlowControlLimit),
} }
packetConfig, err := newPacketConfig(c, log) icmpRouter, err := newICMPRouter(c, log)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("ICMP proxy feature is disabled") log.Warn().Err(err).Msg("ICMP proxy feature is disabled")
} else { } else {
tunnelConfig.PacketConfig = packetConfig tunnelConfig.ICMPRouterServer = icmpRouter
} }
orchestratorConfig := &orchestration.Config{ orchestratorConfig := &orchestration.Config{
Ingress: &ingressRules, Ingress: &ingressRules,
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting), WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
ConfigurationFlags: parseConfigFlags(c), ConfigurationFlags: parseConfigFlags(c),
WriteTimeout: tunnelConfig.WriteStreamTimeout,
} }
return tunnelConfig, orchestratorConfig, nil return tunnelConfig, orchestratorConfig, nil
} }
@ -269,9 +282,9 @@ func parseConfigFlags(c *cli.Context) map[string]string {
} }
func gracePeriod(c *cli.Context) (time.Duration, error) { func gracePeriod(c *cli.Context) (time.Duration, error) {
period := c.Duration("grace-period") period := c.Duration(flags.GracePeriod)
if period > connection.MaxGracePeriod { if period > connection.MaxGracePeriod {
return time.Duration(0), fmt.Errorf("grace-period must be equal or less than %v", connection.MaxGracePeriod) return time.Duration(0), fmt.Errorf("%s must be equal or less than %v", flags.GracePeriod, connection.MaxGracePeriod)
} }
return period, nil return period, nil
} }
@ -280,25 +293,6 @@ func isRunningFromTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd())) return term.IsTerminal(int(os.Stdout.Fd()))
} }
// Remove any duplicates from the slice
func dedup(slice []string) []string {
// Convert the slice into a set
set := make(map[string]bool, 0)
for _, str := range slice {
set[str] = true
}
// Convert the set back into a slice
keys := make([]string, len(set))
i := 0
for str := range set {
keys[i] = str
i++
}
return keys
}
// ParseConfigIPVersion returns the IP version from possible expected values from config // ParseConfigIPVersion returns the IP version from possible expected values from config
func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err error) { func parseConfigIPVersion(version string) (v allregions.ConfigIPVersion, err error) {
switch version { switch version {
@ -359,33 +353,39 @@ func adjustIPVersionByBindAddress(ipVersion allregions.ConfigIPVersion, ip net.I
} }
} }
func newPacketConfig(c *cli.Context, logger *zerolog.Logger) (*ingress.GlobalRouterConfig, error) { func newICMPRouter(c *cli.Context, logger *zerolog.Logger) (ingress.ICMPRouterServer, error) {
ipv4Src, err := determineICMPv4Src(c.String("icmpv4-src"), logger) ipv4Src, ipv6Src, err := determineICMPSources(c, logger)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to determine IPv4 source address for ICMP proxy") return nil, err
} }
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, logger, icmpFunnelTimeout)
if err != nil {
return nil, err
}
return icmpRouter, nil
}
func determineICMPSources(c *cli.Context, logger *zerolog.Logger) (netip.Addr, netip.Addr, error) {
ipv4Src, err := determineICMPv4Src(c.String(flags.ICMPV4Src), logger)
if err != nil {
return netip.Addr{}, netip.Addr{}, errors.Wrap(err, "failed to determine IPv4 source address for ICMP proxy")
}
logger.Info().Msgf("ICMP proxy will use %s as source for IPv4", ipv4Src) logger.Info().Msgf("ICMP proxy will use %s as source for IPv4", ipv4Src)
ipv6Src, zone, err := determineICMPv6Src(c.String("icmpv6-src"), logger, ipv4Src) ipv6Src, zone, err := determineICMPv6Src(c.String(flags.ICMPV6Src), logger, ipv4Src)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to determine IPv6 source address for ICMP proxy") return netip.Addr{}, netip.Addr{}, errors.Wrap(err, "failed to determine IPv6 source address for ICMP proxy")
} }
if zone != "" { if zone != "" {
logger.Info().Msgf("ICMP proxy will use %s in zone %s as source for IPv6", ipv6Src, zone) logger.Info().Msgf("ICMP proxy will use %s in zone %s as source for IPv6", ipv6Src, zone)
} else { } else {
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src) logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
} }
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, zone, logger) return ipv4Src, ipv6Src, nil
if err != nil {
return nil, err
}
return &ingress.GlobalRouterConfig{
ICMPRouter: icmpRouter,
IPv4Src: ipv4Src,
IPv6Src: ipv6Src,
Zone: zone,
}, nil
} }
func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) { func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) {
@ -415,13 +415,12 @@ type interfaceIP struct {
func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) { func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) {
if userDefinedSrc != "" { if userDefinedSrc != "" {
userDefinedIP, zone, _ := strings.Cut(userDefinedSrc, "%") addr, err := netip.ParseAddr(userDefinedSrc)
addr, err := netip.ParseAddr(userDefinedIP)
if err != nil { if err != nil {
return netip.Addr{}, "", err return netip.Addr{}, "", err
} }
if addr.Is6() { if addr.Is6() {
return addr, zone, nil return addr, addr.Zone(), nil
} }
return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc) return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc)
} }

View File

@ -1,5 +1,4 @@
//go:build ignore //go:build ignore
// +build ignore
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+ // TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/credentials"
@ -57,7 +58,7 @@ func newSearchByID(id uuid.UUID, c *cli.Context, log *zerolog.Logger, fs fileSys
} }
func (s searchByID) Path() (string, error) { func (s searchByID) Path() (string, error) {
originCertPath := s.c.String(credentials.OriginCertFlag) originCertPath := s.c.String(cfdflags.OriginCert)
originCertLog := s.log.With(). originCertLog := s.log.With().
Str("originCertPath", originCertPath). Str("originCertPath", originCertPath).
Logger() Logger()

View File

@ -1,3 +0,0 @@
package tunnel
var FipsEnabled bool

View File

@ -139,7 +139,7 @@ func testURLCommand(c *cli.Context) error {
} }
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path) _, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
fmt.Printf("Matched rule #%d\n", i+1) fmt.Printf("Matched rule #%d\n", i)
fmt.Println(ing.Rules[i].MultiLineString()) fmt.Println(ing.Rules[i].MultiLineString())
return nil return nil
} }

View File

@ -19,8 +19,32 @@ import (
) )
const ( const (
baseLoginURL = "https://dash.cloudflare.com/argotunnel" baseLoginURL = "https://dash.cloudflare.com/argotunnel"
callbackStoreURL = "https://login.cloudflareaccess.org/" callbackURL = "https://login.cloudflareaccess.org/"
// For now these are the same but will change in the future once we know which URLs to use (TUN-8872)
fedBaseLoginURL = "https://dash.cloudflare.com/argotunnel"
fedCallbackStoreURL = "https://login.cloudflareaccess.org/"
fedRAMPParamName = "fedramp"
loginURLParamName = "loginURL"
callbackURLParamName = "callbackURL"
)
var (
loginURL = &cli.StringFlag{
Name: loginURLParamName,
Value: baseLoginURL,
Usage: "The URL used to login (default is https://dash.cloudflare.com/argotunnel)",
}
callbackStore = &cli.StringFlag{
Name: callbackURLParamName,
Value: callbackURL,
Usage: "The URL used for the callback (default is https://login.cloudflareaccess.org/)",
}
fedramp = &cli.BoolFlag{
Name: fedRAMPParamName,
Aliases: []string{"f"},
Usage: "Login with FedRAMP High environment.",
}
) )
func buildLoginSubcommand(hidden bool) *cli.Command { func buildLoginSubcommand(hidden bool) *cli.Command {
@ -30,6 +54,11 @@ func buildLoginSubcommand(hidden bool) *cli.Command {
Usage: "Generate a configuration file with your login details", Usage: "Generate a configuration file with your login details",
ArgsUsage: " ", ArgsUsage: " ",
Hidden: hidden, Hidden: hidden,
Flags: []cli.Flag{
loginURL,
callbackStore,
fedramp,
},
} }
} }
@ -38,15 +67,25 @@ func login(c *cli.Context) error {
path, ok, err := checkForExistingCert() path, ok, err := checkForExistingCert()
if ok { if ok {
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path) log.Error().Err(err).Msgf("You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
return nil return nil
} else if err != nil { } else if err != nil {
return err return err
} }
loginURL, err := url.Parse(baseLoginURL) var (
baseloginURL = c.String(loginURLParamName)
callbackStoreURL = c.String(callbackURLParamName)
)
isFEDRamp := c.Bool(fedRAMPParamName)
if isFEDRamp {
baseloginURL = fedBaseLoginURL
callbackStoreURL = fedCallbackStoreURL
}
loginURL, err := url.Parse(baseloginURL)
if err != nil { if err != nil {
// shouldn't happen, URL is hardcoded
return err return err
} }
@ -61,7 +100,23 @@ func login(c *cli.Context) error {
log, log,
) )
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path) 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)
return err
}
cert, err := credentials.DecodeOriginCert(resourceData)
if err != nil {
log.Error().Err(err).Msg("failed to decode origin certificate")
return err
}
if isFEDRamp {
cert.Endpoint = credentials.FedEndpoint
}
resourceData, err = cert.EncodeOriginCert()
if err != nil {
log.Error().Err(err).Msg("failed to encode origin certificate")
return err return err
} }
@ -69,7 +124,7 @@ func login(c *cli.Context) error {
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path)) return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
} }
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path) log.Info().Msgf("You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
return nil return nil
} }

View File

@ -3,6 +3,7 @@ package tunnel
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -10,15 +11,13 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
) )
const httpTimeout = 15 * time.Second const httpTimeout = 15 * time.Second
const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to" + const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee, are subject to the Cloudflare Online Services Terms of Use (https://www.cloudflare.com/website-terms/), and Cloudflare reserves the right to investigate your use of Tunnels for violations of such terms. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"
" experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you " +
"intend to use Tunnels in production you should use a pre-created named tunnel by following: " +
"https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"
// RunQuickTunnel requests a tunnel from the specified service. // RunQuickTunnel requests a tunnel from the specified service.
// We use this to power quick tunnels on trycloudflare.com, but the // We use this to power quick tunnels on trycloudflare.com, but the
@ -35,14 +34,29 @@ func RunQuickTunnel(sc *subcommandContext) error {
Timeout: httpTimeout, Timeout: httpTimeout,
} }
resp, err := client.Post(fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), "application/json", nil) req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), nil)
if err != nil {
return errors.Wrap(err, "failed to build quick tunnel request")
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", buildInfo.UserAgent())
resp, err := client.Do(req)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to request quick Tunnel") return errors.Wrap(err, "failed to request quick Tunnel")
} }
defer resp.Body.Close() defer 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)
if err != nil {
return errors.Wrap(err, "failed to read quick-tunnel response")
}
var data QuickTunnelResponse var data QuickTunnelResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { if err := json.Unmarshal(rsp_body, &data); err != nil {
rsp_string := string(rsp_body)
fields := map[string]interface{}{"status_code": resp.Status}
sc.log.Err(err).Fields(fields).Msgf("Error unmarshaling QuickTunnel response: %s", rsp_string)
return errors.Wrap(err, "failed to unmarshal quick Tunnel") return errors.Wrap(err, "failed to unmarshal quick Tunnel")
} }
@ -69,17 +83,17 @@ func RunQuickTunnel(sc *subcommandContext) error {
sc.log.Info().Msg(line) sc.log.Info().Msg(line)
} }
if !sc.c.IsSet("protocol") { if !sc.c.IsSet(flags.Protocol) {
sc.c.Set("protocol", "quic") _ = sc.c.Set(flags.Protocol, "quic")
} }
// Override the number of connections used. Quick tunnels shouldn't be used for production usage, // Override the number of connections used. Quick tunnels shouldn't be used for production usage,
// so, use a single connection instead. // so, use a single connection instead.
sc.c.Set(haConnectionsFlag, "1") _ = sc.c.Set(flags.HaConnections, "1")
return StartServer( return StartServer(
sc.c, sc.c,
buildInfo, buildInfo,
&connection.NamedTunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname}, &connection.TunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
sc.log, sc.log,
) )
} }

View File

@ -1,5 +1,4 @@
//go:build !windows //go:build !windows
// +build !windows
package tunnel package tunnel

View File

@ -9,22 +9,26 @@ import (
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cfapi"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/credentials"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
) )
type errInvalidJSONCredential struct { const fedRampBaseApiURL = "https://api.fed.cloudflare.com/client/v4"
type invalidJSONCredentialError struct {
err error err error
path string path string
} }
func (e errInvalidJSONCredential) Error() string { func (e invalidJSONCredentialError) Error() string {
return "Invalid JSON when parsing tunnel credentials file" return "Invalid JSON when parsing tunnel credentials file"
} }
@ -51,7 +55,12 @@ func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
// Returns something that can find the given tunnel's credentials file. // Returns something that can find the given tunnel's credentials file.
func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder { func (sc *subcommandContext) credentialFinder(tunnelID uuid.UUID) CredFinder {
if path := sc.c.String(CredFileFlag); path != "" { if path := sc.c.String(CredFileFlag); path != "" {
return newStaticPath(path, sc.fs) // Expand path if CredFileFlag contains `~`
absPath, err := homedir.Expand(path)
if err != nil {
return newStaticPath(path, sc.fs)
}
return newStaticPath(absPath, sc.fs)
} }
return newSearchByID(tunnelID, sc.c, sc.log, sc.fs) return newSearchByID(tunnelID, sc.c, sc.log, sc.fs)
} }
@ -64,7 +73,16 @@ func (sc *subcommandContext) client() (cfapi.Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
sc.tunnelstoreClient, err = cred.Client(sc.c.String("api-url"), buildInfo.UserAgent(), sc.log)
var apiURL string
if cred.IsFEDEndpoint() {
sc.log.Info().Str("api-url", fedRampBaseApiURL).Msg("using fedramp base api")
apiURL = fedRampBaseApiURL
} else {
apiURL = sc.c.String(cfdflags.ApiURL)
}
sc.tunnelstoreClient, err = cred.Client(apiURL, buildInfo.UserAgent(), sc.log)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -73,7 +91,7 @@ func (sc *subcommandContext) client() (cfapi.Client, error) {
func (sc *subcommandContext) credential() (*credentials.User, error) { func (sc *subcommandContext) credential() (*credentials.User, error) {
if sc.userCredential == nil { if sc.userCredential == nil {
uc, err := credentials.Read(sc.c.String(credentials.OriginCertFlag), sc.log) uc, err := credentials.Read(sc.c.String(cfdflags.OriginCert), sc.log)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -94,13 +112,13 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne
var credentials connection.Credentials var credentials connection.Credentials
if err = json.Unmarshal(body, &credentials); err != nil { if err = json.Unmarshal(body, &credentials); err != nil {
if strings.HasSuffix(filePath, ".pem") { if filepath.Ext(filePath) == ".pem" {
return connection.Credentials{}, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. " + return connection.Credentials{}, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. " +
"The tunnel credentials file was originally created by `cloudflared tunnel create`. " + "The tunnel credentials file was originally created by `cloudflared tunnel create`. " +
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " + "You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel " +
"login`.") "login`.")
} }
return connection.Credentials{}, errInvalidJSONCredential{path: filePath, err: err} return connection.Credentials{}, invalidJSONCredentialError{path: filePath, err: err}
} }
return credentials, nil return credentials, nil
} }
@ -122,7 +140,7 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Couldn't decode tunnel secret from base64") return nil, errors.Wrap(err, "Couldn't decode tunnel secret from base64")
} }
tunnelSecret = []byte(decodedSecret) tunnelSecret = decodedSecret
if len(tunnelSecret) < 32 { if len(tunnelSecret) < 32 {
return nil, errors.New("Decoded tunnel secret must be at least 32 bytes long") return nil, errors.New("Decoded tunnel secret must be at least 32 bytes long")
} }
@ -156,11 +174,11 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
var errorLines []string var errorLines []string
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath)) errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write tunnel credentials to %s.", tunnel.Name, tunnel.ID, credentialsFilePath))
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr)) errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil { if deleteErr := client.DeleteTunnel(tunnel.ID, true); deleteErr != nil {
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID)) errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr)) errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
} else { } else {
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the credentials file")) errorLines = append(errorLines, "The tunnel was deleted, because the tunnel can't be run without the credentials file")
} }
errorMsg := strings.Join(errorLines, "\n") errorMsg := strings.Join(errorLines, "\n")
return nil, errors.New(errorMsg) return nil, errors.New(errorMsg)
@ -189,7 +207,7 @@ func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel,
} }
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error { func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
forceFlagSet := sc.c.Bool("force") forceFlagSet := sc.c.Bool(cfdflags.Force)
client, err := sc.client() client, err := sc.client()
if err != nil { if err != nil {
@ -206,13 +224,8 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
if !tunnel.DeletedAt.IsZero() { if !tunnel.DeletedAt.IsZero() {
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID) return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
} }
if forceFlagSet {
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
}
}
if err := client.DeleteTunnel(tunnel.ID); err != nil { if err := client.DeleteTunnel(tunnel.ID, forceFlagSet); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID) return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
} }
@ -234,7 +247,7 @@ func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Cre
var err error var err error
if credentialsContents := sc.c.String(CredContentsFlag); credentialsContents != "" { if credentialsContents := sc.c.String(CredContentsFlag); credentialsContents != "" {
if err = json.Unmarshal([]byte(credentialsContents), &credentials); err != nil { if err = json.Unmarshal([]byte(credentialsContents), &credentials); err != nil {
err = errInvalidJSONCredential{path: "TUNNEL_CRED_CONTENTS", err: err} err = invalidJSONCredentialError{path: "TUNNEL_CRED_CONTENTS", err: err}
} }
} else { } else {
credFinder := sc.credentialFinder(tunnelID) credFinder := sc.credentialFinder(tunnelID)
@ -250,7 +263,7 @@ func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Cre
func (sc *subcommandContext) run(tunnelID uuid.UUID) error { func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
credentials, err := sc.findCredentials(tunnelID) credentials, err := sc.findCredentials(tunnelID)
if err != nil { if err != nil {
if e, ok := err.(errInvalidJSONCredential); ok { if e, ok := err.(invalidJSONCredentialError); ok {
sc.log.Error().Msgf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path) sc.log.Error().Msgf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path)
sc.log.Error().Msgf("Invalid JSON when parsing credentials file: %s", e.err.Error()) sc.log.Error().Msgf("Invalid JSON when parsing credentials file: %s", e.err.Error())
} }
@ -266,7 +279,7 @@ func (sc *subcommandContext) runWithCredentials(credentials connection.Credentia
return StartServer( return StartServer(
sc.c, sc.c,
buildInfo, buildInfo,
&connection.NamedTunnelProperties{Credentials: credentials}, &connection.TunnelProperties{Credentials: credentials},
sc.log, sc.log,
) )
} }

View File

@ -1,6 +1,9 @@
package tunnel package tunnel
import ( import (
"net"
"github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cfapi"
@ -24,12 +27,12 @@ func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, err
return client.AddRoute(newRoute) return client.AddRoute(newRoute)
} }
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error { func (sc *subcommandContext) deleteRoute(id uuid.UUID) error {
client, err := sc.client() client, err := sc.client()
if err != nil { if err != nil {
return errors.Wrap(err, noClientMsg) return errors.Wrap(err, noClientMsg)
} }
return client.DeleteRoute(params) return client.DeleteRoute(id)
} }
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) { func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
@ -39,3 +42,25 @@ func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfap
} }
return client.GetByIP(params) return client.GetByIP(params)
} }
func (sc *subcommandContext) getRouteId(network net.IPNet, vnetId *uuid.UUID) (uuid.UUID, error) {
filters := cfapi.NewIPRouteFilter()
filters.NotDeleted()
filters.NetworkIsSubsetOf(network)
filters.NetworkIsSupersetOf(network)
if vnetId != nil {
filters.VNetID(*vnetId)
}
result, err := sc.listRoutes(filters)
if err != nil {
return uuid.Nil, err
}
if len(result) != 1 {
return uuid.Nil, errors.New("unable to find route for provided network and vnet")
}
return result[0].ID, nil
}

View File

@ -219,7 +219,7 @@ func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, erro
return "token", nil return "token", nil
} }
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error { func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
tunnel, ok := d.mockTunnels[tunnelID] tunnel, ok := d.mockTunnels[tunnelID]
if !ok { if !ok {
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID) return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)

View File

@ -5,6 +5,8 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -14,28 +16,40 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
homedir "github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc" "github.com/urfave/cli/v2/altsrc"
"golang.org/x/net/idna" "golang.org/x/net/idna"
yaml "gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/diagnostic"
"github.com/cloudflare/cloudflared/fips"
"github.com/cloudflare/cloudflared/metrics"
) )
const ( const (
allSortByOptions = "name, id, createdAt, deletedAt, numConnections" allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
connsSortByOptions = "id, startedAt, numConnections, version" connsSortByOptions = "id, startedAt, numConnections, version"
CredFileFlagAlias = "cred-file" CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file" CredFileFlag = "credentials-file"
CredContentsFlag = "credentials-contents" CredContentsFlag = "credentials-contents"
TunnelTokenFlag = "token" TunnelTokenFlag = "token"
overwriteDNSFlagName = "overwrite-dns" TunnelTokenFileFlag = "token-file"
overwriteDNSFlagName = "overwrite-dns"
noDiagLogsFlagName = "no-diag-logs"
noDiagMetricsFlagName = "no-diag-metrics"
noDiagSystemFlagName = "no-diag-system"
noDiagRuntimeFlagName = "no-diag-runtime"
noDiagNetworkFlagName = "no-diag-network"
diagContainerIDFlagName = "diag-container-id"
diagPodFlagName = "diag-pod-id"
LogFieldTunnelID = "tunnelID" LogFieldTunnelID = "tunnelID"
) )
@ -47,7 +61,7 @@ var (
Usage: "Include deleted tunnels in the list", Usage: "Include deleted tunnels in the list",
} }
listNameFlag = &cli.StringFlag{ listNameFlag = &cli.StringFlag{
Name: "name", Name: flags.Name,
Aliases: []string{"n"}, Aliases: []string{"n"},
Usage: "List tunnels with the given `NAME`", Usage: "List tunnels with the given `NAME`",
} }
@ -95,7 +109,7 @@ var (
EnvVars: []string{"TUNNEL_LIST_INVERT_SORT"}, EnvVars: []string{"TUNNEL_LIST_INVERT_SORT"},
} }
featuresFlag = altsrc.NewStringSliceFlag(&cli.StringSliceFlag{ featuresFlag = altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "features", Name: flags.Features,
Aliases: []string{"F"}, Aliases: []string{"F"},
Usage: "Opt into various features that are still being developed or tested.", Usage: "Opt into various features that are still being developed or tested.",
}) })
@ -113,18 +127,23 @@ var (
}) })
tunnelTokenFlag = altsrc.NewStringFlag(&cli.StringFlag{ tunnelTokenFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: TunnelTokenFlag, Name: TunnelTokenFlag,
Usage: "The Tunnel token. When provided along with credentials, this will take precedence.", Usage: "The Tunnel token. When provided along with credentials, this will take precedence. Also takes precedence over token-file",
EnvVars: []string{"TUNNEL_TOKEN"}, EnvVars: []string{"TUNNEL_TOKEN"},
}) })
tunnelTokenFileFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: TunnelTokenFileFlag,
Usage: "Filepath at which to read the tunnel token. When provided along with credentials, this will take precedence.",
EnvVars: []string{"TUNNEL_TOKEN_FILE"},
})
forceDeleteFlag = &cli.BoolFlag{ forceDeleteFlag = &cli.BoolFlag{
Name: "force", Name: flags.Force,
Aliases: []string{"f"}, Aliases: []string{"f"},
Usage: "Cleans up any stale connections before the tunnel is deleted. cloudflared will not " + Usage: "Deletes a tunnel even if tunnel is connected and it has dependencies associated to it. (eg. IP routes)." +
"delete a tunnel with connections without this flag.", " It is not possible to delete tunnels that have connections or non-deleted dependencies, without this flag.",
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"}, EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
} }
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{ selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: "protocol", Name: flags.Protocol,
Value: connection.AutoSelectFlag, Value: connection.AutoSelectFlag,
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: fmt.Sprintf("Protocol implementation to connect with Cloudflare's edge network. %s", connection.AvailableProtocolFlagMessage), Usage: fmt.Sprintf("Protocol implementation to connect with Cloudflare's edge network. %s", connection.AvailableProtocolFlagMessage),
@ -132,11 +151,11 @@ var (
Hidden: true, Hidden: true,
}) })
postQuantumFlag = altsrc.NewBoolFlag(&cli.BoolFlag{ postQuantumFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "post-quantum", Name: flags.PostQuantum,
Usage: "When given creates an experimental post-quantum secure tunnel", Usage: "When given creates an experimental post-quantum secure tunnel",
Aliases: []string{"pq"}, Aliases: []string{"pq"},
EnvVars: []string{"TUNNEL_POST_QUANTUM"}, EnvVars: []string{"TUNNEL_POST_QUANTUM"},
Hidden: FipsEnabled, Hidden: fips.IsFipsEnabled(),
}) })
sortInfoByFlag = &cli.StringFlag{ sortInfoByFlag = &cli.StringFlag{
Name: "sort-by", Name: "sort-by",
@ -168,15 +187,60 @@ var (
EnvVars: []string{"TUNNEL_CREATE_SECRET"}, EnvVars: []string{"TUNNEL_CREATE_SECRET"},
} }
icmpv4SrcFlag = &cli.StringFlag{ icmpv4SrcFlag = &cli.StringFlag{
Name: "icmpv4-src", Name: flags.ICMPV4Src,
Usage: "Source address to send/receive ICMPv4 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to 0.0.0.0.", Usage: "Source address to send/receive ICMPv4 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to 0.0.0.0.",
EnvVars: []string{"TUNNEL_ICMPV4_SRC"}, EnvVars: []string{"TUNNEL_ICMPV4_SRC"},
} }
icmpv6SrcFlag = &cli.StringFlag{ icmpv6SrcFlag = &cli.StringFlag{
Name: "icmpv6-src", Name: flags.ICMPV6Src,
Usage: "Source address and the interface name to send/receive ICMPv6 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to ::.", Usage: "Source address and the interface name to send/receive ICMPv6 messages. If not provided cloudflared will dial a local address to determine the source IP or fallback to ::.",
EnvVars: []string{"TUNNEL_ICMPV6_SRC"}, EnvVars: []string{"TUNNEL_ICMPV6_SRC"},
} }
metricsFlag = &cli.StringFlag{
Name: flags.Metrics,
Usage: "The metrics server address i.e.: 127.0.0.1:12345. If your instance is running in a Docker/Kubernetes environment you need to setup port forwarding for your application.",
Value: "",
}
diagContainerFlag = &cli.StringFlag{
Name: diagContainerIDFlagName,
Usage: "Container ID or Name to collect logs from",
Value: "",
}
diagPodFlag = &cli.StringFlag{
Name: diagPodFlagName,
Usage: "Kubernetes POD to collect logs from",
Value: "",
}
noDiagLogsFlag = &cli.BoolFlag{
Name: noDiagLogsFlagName,
Usage: "Log collection will not be performed",
Value: false,
}
noDiagMetricsFlag = &cli.BoolFlag{
Name: noDiagMetricsFlagName,
Usage: "Metric collection will not be performed",
Value: false,
}
noDiagSystemFlag = &cli.BoolFlag{
Name: noDiagSystemFlagName,
Usage: "System information collection will not be performed",
Value: false,
}
noDiagRuntimeFlag = &cli.BoolFlag{
Name: noDiagRuntimeFlagName,
Usage: "Runtime information collection will not be performed",
Value: false,
}
noDiagNetworkFlag = &cli.BoolFlag{
Name: noDiagNetworkFlagName,
Usage: "Network diagnostics won't be performed",
Value: false,
}
maxActiveFlowsFlag = &cli.Uint64Flag{
Name: flags.MaxActiveFlows,
Usage: "Overrides the remote configuration for max active private network flows (TCP/UDP) that this cloudflared instance supports",
EnvVars: []string{"TUNNEL_MAX_ACTIVE_FLOWS"},
}
) )
func buildCreateCommand() *cli.Command { func buildCreateCommand() *cli.Command {
@ -279,7 +343,7 @@ func listCommand(c *cli.Context) error {
if !c.Bool("show-deleted") { if !c.Bool("show-deleted") {
filter.NoDeleted() filter.NoDeleted()
} }
if name := c.String("name"); name != "" { if name := c.String(flags.Name); name != "" {
filter.ByName(name) filter.ByName(name)
} }
if namePrefix := c.String("name-prefix"); namePrefix != "" { if namePrefix := c.String("name-prefix"); namePrefix != "" {
@ -373,7 +437,6 @@ func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected
} }
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string { func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
// Count connections per colo // Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections)) numConnsPerColo := make(map[string]uint, len(connections))
for _, connection := range connections { for _, connection := range connections {
@ -390,13 +453,51 @@ func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected boo
sort.Strings(sortedColos) sort.Strings(sortedColos)
// Map each colo to its frequency, combine into output string. // Map each colo to its frequency, combine into output string.
var output []string output := make([]string, 0, len(sortedColos))
for _, coloName := range sortedColos { for _, coloName := range sortedColos {
output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName)) output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName))
} }
return strings.Join(output, ", ") return strings.Join(output, ", ")
} }
func buildReadyCommand() *cli.Command {
return &cli.Command{
Name: "ready",
Action: cliutil.ConfiguredAction(readyCommand),
Usage: "Call /ready endpoint and return proper exit code",
UsageText: "cloudflared tunnel [tunnel command options] ready [subcommand options]",
Description: "cloudflared tunnel ready will return proper exit code based on the /ready endpoint",
Flags: []cli.Flag{},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func readyCommand(c *cli.Context) error {
metricsOpts := c.String(flags.Metrics)
if !c.IsSet(flags.Metrics) {
return errors.New("--metrics has to be provided")
}
requestURL := fmt.Sprintf("http://%s/ready", metricsOpts)
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
return fmt.Errorf("http://%s/ready endpoint returned status code %d\n%s", metricsOpts, res.StatusCode, body)
}
return nil
}
func buildInfoCommand() *cli.Command { func buildInfoCommand() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "info", Name: "info",
@ -613,8 +714,10 @@ func buildRunCommand() *cli.Command {
selectProtocolFlag, selectProtocolFlag,
featuresFlag, featuresFlag,
tunnelTokenFlag, tunnelTokenFlag,
tunnelTokenFileFlag,
icmpv4SrcFlag, icmpv4SrcFlag,
icmpv6SrcFlag, icmpv6SrcFlag,
maxActiveFlowsFlag,
} }
flags = append(flags, configureProxyFlags(false)...) flags = append(flags, configureProxyFlags(false)...)
return &cli.Command{ return &cli.Command{
@ -652,12 +755,22 @@ func runCommand(c *cli.Context) error {
"your origin will not be reachable. You should remove the `hostname` property to avoid this warning.") "your origin will not be reachable. You should remove the `hostname` property to avoid this warning.")
} }
tokenStr := c.String(TunnelTokenFlag)
// Check if tokenStr is blank before checking for tokenFile
if tokenStr == "" {
if tokenFile := c.String(TunnelTokenFileFlag); tokenFile != "" {
data, err := os.ReadFile(tokenFile)
if err != nil {
return cliutil.UsageError("Failed to read token file: " + err.Error())
}
tokenStr = strings.TrimSpace(string(data))
}
}
// Check if token is provided and if not use default tunnelID flag method // Check if token is provided and if not use default tunnelID flag method
if tokenStr := c.String(TunnelTokenFlag); tokenStr != "" { if tokenStr != "" {
if token, err := ParseToken(tokenStr); err == nil { if token, err := ParseToken(tokenStr); err == nil {
return sc.runWithCredentials(token.Credentials()) return sc.runWithCredentials(token.Credentials())
} }
return cliutil.UsageError("Provided Tunnel token is not valid.") return cliutil.UsageError("Provided Tunnel token is not valid.")
} else { } else {
tunnelRef := c.Args().First() tunnelRef := c.Args().First()
@ -862,8 +975,10 @@ func lbRouteFromArg(c *cli.Context) (cfapi.HostnameRoute, error) {
return cfapi.NewLBRoute(lbName, lbPool), nil return cfapi.NewLBRoute(lbName, lbPool), nil
} }
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$") var (
var hostNameRegex = regexp.MustCompile("^[*_a-zA-Z0-9][-_.a-zA-Z0-9]*$") nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
hostNameRegex = regexp.MustCompile("^[*_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
)
func validateName(s string, allowWildcardSubdomain bool) bool { func validateName(s string, allowWildcardSubdomain bool) bool {
if allowWildcardSubdomain { if allowWildcardSubdomain {
@ -951,3 +1066,78 @@ SUBCOMMAND OPTIONS:
` `
return fmt.Sprintf(template, parentFlagsHelp) return fmt.Sprintf(template, parentFlagsHelp)
} }
func buildDiagCommand() *cli.Command {
return &cli.Command{
Name: "diag",
Action: cliutil.ConfiguredAction(diagCommand),
Usage: "Creates a diagnostic report from a local cloudflared instance",
UsageText: "cloudflared tunnel [tunnel command options] diag [subcommand options]",
Description: "cloudflared tunnel diag will create a diagnostic report of a local cloudflared instance. The diagnostic procedure collects: logs, metrics, system information, traceroute to Cloudflare Edge, and runtime information. Since there may be multiple instances of cloudflared running the --metrics option may be provided to target a specific instance.",
Flags: []cli.Flag{
metricsFlag,
diagContainerFlag,
diagPodFlag,
noDiagLogsFlag,
noDiagMetricsFlag,
noDiagSystemFlag,
noDiagRuntimeFlag,
noDiagNetworkFlag,
},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func diagCommand(ctx *cli.Context) error {
sctx, err := newSubcommandContext(ctx)
if err != nil {
return err
}
log := sctx.log
options := diagnostic.Options{
KnownAddresses: metrics.GetMetricsKnownAddresses(metrics.Runtime),
Address: sctx.c.String(flags.Metrics),
ContainerID: sctx.c.String(diagContainerIDFlagName),
PodID: sctx.c.String(diagPodFlagName),
Toggles: diagnostic.Toggles{
NoDiagLogs: sctx.c.Bool(noDiagLogsFlagName),
NoDiagMetrics: sctx.c.Bool(noDiagMetricsFlagName),
NoDiagSystem: sctx.c.Bool(noDiagSystemFlagName),
NoDiagRuntime: sctx.c.Bool(noDiagRuntimeFlagName),
NoDiagNetwork: sctx.c.Bool(noDiagNetworkFlagName),
},
}
if options.Address == "" {
log.Info().Msg("If your instance is running in a Docker/Kubernetes environment you need to setup port forwarding for your application.")
}
states, err := diagnostic.RunDiagnostic(log, options)
if errors.Is(err, diagnostic.ErrMetricsServerNotFound) {
log.Warn().Msg("No instances found")
return nil
}
if errors.Is(err, diagnostic.ErrMultipleMetricsServerFound) {
if states != nil {
log.Info().Msgf("Found multiple instances running:")
for _, state := range states {
log.Info().Msgf("Instance: tunnel-id=%s connector-id=%s metrics-address=%s", state.TunnelID, state.ConnectorID, state.URL.String())
}
log.Info().Msgf("To select one instance use the option --metrics")
}
return nil
}
if errors.Is(err, diagnostic.ErrLogConfigurationIsInvalid) {
log.Info().Msg("Couldn't extract logs from the instance. If the instance is running in a containerized environment use the option --diag-container-id or --diag-pod-id. If there is no logging configuration use --no-diag-logs.")
}
if err != nil {
log.Warn().Msg("Diagnostic completed with one or more errors")
} else {
log.Info().Msg("Diagnostic completed")
}
return nil
}

View File

@ -4,23 +4,23 @@ import (
"fmt" "fmt"
"regexp" "regexp"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
// Restrict key names to characters allowed in an HTTP header name. // Restrict key names to characters allowed in an HTTP header name.
// Restrict key values to printable characters (what is recognised as data in an HTTP header value). // Restrict key values to printable characters (what is recognised as data in an HTTP header value).
var tagRegexp = regexp.MustCompile("^([a-zA-Z0-9!#$%&'*+\\-.^_`|~]+)=([[:print:]]+)$") var tagRegexp = regexp.MustCompile("^([a-zA-Z0-9!#$%&'*+\\-.^_`|~]+)=([[:print:]]+)$")
func NewTagFromCLI(compoundTag string) (tunnelpogs.Tag, bool) { func NewTagFromCLI(compoundTag string) (pogs.Tag, bool) {
matches := tagRegexp.FindStringSubmatch(compoundTag) matches := tagRegexp.FindStringSubmatch(compoundTag)
if len(matches) == 0 { if len(matches) == 0 {
return tunnelpogs.Tag{}, false return pogs.Tag{}, false
} }
return tunnelpogs.Tag{Name: matches[1], Value: matches[2]}, true return pogs.Tag{Name: matches[1], Value: matches[2]}, true
} }
func NewTagSliceFromCLI(tags []string) ([]tunnelpogs.Tag, error) { func NewTagSliceFromCLI(tags []string) ([]pogs.Tag, error) {
var tagSlice []tunnelpogs.Tag var tagSlice []pogs.Tag
for _, compoundTag := range tags { for _, compoundTag := range tags {
if tag, ok := NewTagFromCLI(compoundTag); ok { if tag, ok := NewTagFromCLI(compoundTag); ok {
tagSlice = append(tagSlice, tag) tagSlice = append(tagSlice, tag)

View File

@ -3,7 +3,7 @@ package tunnel
import ( import (
"testing" "testing"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -11,12 +11,12 @@ import (
func TestSingleTag(t *testing.T) { func TestSingleTag(t *testing.T) {
testCases := []struct { testCases := []struct {
Input string Input string
Output tunnelpogs.Tag Output pogs.Tag
Fail bool Fail bool
}{ }{
{Input: "x=y", Output: tunnelpogs.Tag{Name: "x", Value: "y"}}, {Input: "x=y", Output: pogs.Tag{Name: "x", Value: "y"}},
{Input: "More-Complex=Tag Values", Output: tunnelpogs.Tag{Name: "More-Complex", Value: "Tag Values"}}, {Input: "More-Complex=Tag Values", Output: pogs.Tag{Name: "More-Complex", Value: "Tag Values"}},
{Input: "First=Equals=Wins", Output: tunnelpogs.Tag{Name: "First", Value: "Equals=Wins"}}, {Input: "First=Equals=Wins", Output: pogs.Tag{Name: "First", Value: "Equals=Wins"}},
{Input: "x=", Fail: true}, {Input: "x=", Fail: true},
{Input: "=y", Fail: true}, {Input: "=y", Fail: true},
{Input: "=", Fail: true}, {Input: "=", Fail: true},

View File

@ -21,6 +21,8 @@ var (
Aliases: []string{"vn"}, Aliases: []string{"vn"},
Usage: "The ID or name of the virtual network to which the route is associated to.", Usage: "The ID or name of the virtual network to which the route is associated to.",
} }
errAddRoute = errors.New("You must supply exactly one argument, the ID or CIDR of the route you want to delete")
) )
func buildRouteIPSubcommand() *cli.Command { func buildRouteIPSubcommand() *cli.Command {
@ -30,7 +32,7 @@ func buildRouteIPSubcommand() *cli.Command {
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]", UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
Description: `cloudflared can provision routes for any IP space in your corporate network. Users enrolled in Description: `cloudflared can provision routes for any IP space in your corporate network. Users enrolled in
your Cloudflare for Teams organization can reach those IPs through the Cloudflare WARP your Cloudflare for Teams organization can reach those IPs through the Cloudflare WARP
client. You can then configure L7/L4 filtering on https://dash.teams.cloudflare.com to client. You can then configure L7/L4 filtering on https://one.dash.cloudflare.com to
determine who can reach certain routes. determine who can reach certain routes.
By default IP routes all exist within a single virtual network. If you use the same IP By default IP routes all exist within a single virtual network. If you use the same IP
space(s) in different physical private networks, all meant to be reachable via IP routes, space(s) in different physical private networks, all meant to be reachable via IP routes,
@ -68,11 +70,9 @@ which virtual network's routing table you want to add the route to with:
Name: "delete", Name: "delete",
Action: cliutil.ConfiguredAction(deleteRouteCommand), Action: cliutil.ConfiguredAction(deleteRouteCommand),
Usage: "Delete a row from your organization's private routing table", Usage: "Delete a row from your organization's private routing table",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]", UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [Route ID or CIDR]",
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network Description: `Deletes the row for the given route ID from your routing table. That portion of your network
will no longer be reachable by the WARP clients. Note that if you use virtual will no longer be reachable.`,
networks, then you have to tell which virtual network whose routing table you
have a row deleted from.`,
Flags: []cli.Flag{vnetFlag}, Flags: []cli.Flag{vnetFlag},
}, },
{ {
@ -187,33 +187,36 @@ func deleteRouteCommand(c *cli.Context) error {
} }
if c.NArg() != 1 { if c.NArg() != 1 {
return errors.New("You must supply exactly one argument, the network whose route you want to delete (in CIDR form e.g. 1.2.3.4/32)") return errAddRoute
} }
_, network, err := net.ParseCIDR(c.Args().First()) var routeId uuid.UUID
routeId, err = uuid.Parse(c.Args().First())
if err != nil { if err != nil {
return errors.Wrap(err, "Invalid network CIDR") _, network, err := net.ParseCIDR(c.Args().First())
} if err != nil || network == nil {
if network == nil { return errAddRoute
return errors.New("Invalid network CIDR") }
}
params := cfapi.DeleteRouteParams{ var vnetId *uuid.UUID
Network: *network, if c.IsSet(vnetFlag.Name) {
} id, err := getVnetId(sc, c.String(vnetFlag.Name))
if err != nil {
return err
}
vnetId = &id
}
if c.IsSet(vnetFlag.Name) { routeId, err = sc.getRouteId(*network, vnetId)
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
if err != nil { if err != nil {
return err return err
} }
params.VNetID = &vnetId
} }
if err := sc.deleteRoute(params); err != nil { if err := sc.deleteRoute(routeId); err != nil {
return errors.Wrap(err, "API error") return errors.Wrap(err, "API error")
} }
fmt.Printf("Successfully deleted route for %s\n", network) fmt.Printf("Successfully deleted route with ID %s\n", routeId)
return nil return nil
} }
@ -269,7 +272,7 @@ func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
defer writer.Flush() defer writer.Flush()
// Print column headers with tabbed columns // Print column headers with tabbed columns
_, _ = fmt.Fprintln(writer, "NETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t") _, _ = fmt.Fprintln(writer, "ID\tNETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
// Loop through routes, create formatted string for each, and print using tabwriter // Loop through routes, create formatted string for each, and print using tabwriter
for _, route := range routes { for _, route := range routes {

View File

@ -14,13 +14,15 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"golang.org/x/term" "golang.org/x/term"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
) )
const ( const (
DefaultCheckUpdateFreq = time.Hour * 24 DefaultCheckUpdateFreq = time.Hour * 24
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/as-a-service/" noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configure-tunnels/local-management/as-a-service/"
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems." noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager." noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
isManagedInstallFile = ".installedFromPackageManager" isManagedInstallFile = ".installedFromPackageManager"
@ -31,12 +33,13 @@ const (
) )
var ( var (
version string buildInfo *cliutil.BuildInfo
BuiltForPackageManager = "" BuiltForPackageManager = ""
) )
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11 // BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder // https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
// nolint: errname
type statusSuccess struct { type statusSuccess struct {
newVersion string newVersion string
} }
@ -49,16 +52,16 @@ func (u *statusSuccess) ExitCode() int {
return 11 return 11
} }
// UpdateErr implements ExitCoder interface, the app will exit with status code 10 // statusError implements ExitCoder interface, the app will exit with status code 10
type statusErr struct { type statusError struct {
err error err error
} }
func (e *statusErr) Error() string { func (e *statusError) Error() string {
return fmt.Sprintf("failed to update cloudflared: %v", e.err) return fmt.Sprintf("failed to update cloudflared: %v", e.err)
} }
func (e *statusErr) ExitCode() int { func (e *statusError) ExitCode() int {
return 10 return 10
} }
@ -78,11 +81,11 @@ type UpdateOutcome struct {
} }
func (uo *UpdateOutcome) noUpdate() bool { func (uo *UpdateOutcome) noUpdate() bool {
return uo.Error == nil && uo.Updated == false return uo.Error == nil && !uo.Updated
} }
func Init(v string) { func Init(info *cliutil.BuildInfo) {
version = v buildInfo = info
} }
func CheckForUpdate(options updateOptions) (CheckResult, error) { func CheckForUpdate(options updateOptions) (CheckResult, error) {
@ -100,11 +103,12 @@ func CheckForUpdate(options updateOptions) (CheckResult, error) {
cfdPath = encodeWindowsPath(cfdPath) cfdPath = encodeWindowsPath(cfdPath)
} }
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta, s := NewWorkersService(buildInfo.CloudflaredVersion, url, cfdPath, Options{IsBeta: options.isBeta,
IsForced: options.isForced, RequestedVersion: options.intendedVersion}) IsForced: options.isForced, RequestedVersion: options.intendedVersion})
return s.Check() return s.Check()
} }
func encodeWindowsPath(path string) string { func encodeWindowsPath(path string) string {
// We do this because Windows allows spaces in directories such as // We do this because Windows allows spaces in directories such as
// Program Files but does not allow these directories to be spaced in batch files. // Program Files but does not allow these directories to be spaced in batch files.
@ -151,7 +155,7 @@ func Update(c *cli.Context) error {
log.Info().Msg("cloudflared is set to update from staging") log.Info().Msg("cloudflared is set to update from staging")
} }
isForced := c.Bool("force") isForced := c.Bool(cfdflags.Force)
if isForced { if isForced {
log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version") log.Info().Msg("cloudflared is set to upgrade to the latest publish version regardless of the current version")
} }
@ -164,7 +168,7 @@ func Update(c *cli.Context) error {
intendedVersion: c.String("version"), intendedVersion: c.String("version"),
}) })
if updateOutcome.Error != nil { if updateOutcome.Error != nil {
return &statusErr{updateOutcome.Error} return &statusError{updateOutcome.Error}
} }
if updateOutcome.noUpdate() { if updateOutcome.noUpdate() {
@ -196,10 +200,9 @@ func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome {
// AutoUpdater periodically checks for new version of cloudflared. // AutoUpdater periodically checks for new version of cloudflared.
type AutoUpdater struct { type AutoUpdater struct {
configurable *configurable configurable *configurable
listeners *gracenet.Net listeners *gracenet.Net
updateConfigChan chan *configurable log *zerolog.Logger
log *zerolog.Logger
} }
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime // AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
@ -210,10 +213,9 @@ type configurable struct {
func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater { func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
return &AutoUpdater{ return &AutoUpdater{
configurable: createUpdateConfig(updateDisabled, freq, log), configurable: createUpdateConfig(updateDisabled, freq, log),
listeners: listeners, listeners: listeners,
updateConfigChan: make(chan *configurable), log: log,
log: log,
} }
} }
@ -232,19 +234,27 @@ func createUpdateConfig(updateDisabled bool, freq time.Duration, log *zerolog.Lo
} }
} }
// Run will perodically check for cloudflared updates, download them, and then restart the current cloudflared process
// to use the new version. It delays the first update check by the configured frequency as to not attempt a
// download immediately and restart after starting (in the case that there is an upgrade available).
func (a *AutoUpdater) Run(ctx context.Context) error { func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq) ticker := time.NewTicker(a.configurable.freq)
for { for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled}) updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
if updateOutcome.Updated { if updateOutcome.Updated {
Init(updateOutcome.Version) buildInfo.CloudflaredVersion = updateOutcome.Version
if IsSysV() { if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process // SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.log.Info().Msg("Restarting service managed by SysV...") a.log.Info().Msg("Restarting service managed by SysV...")
pid, err := a.listeners.StartProcess() pid, err := a.listeners.StartProcess()
if err != nil { if err != nil {
a.log.Err(err).Msg("Unable to restart server automatically") a.log.Err(err).Msg("Unable to restart server automatically")
return &statusErr{err: err} return &statusError{err: err}
} }
// stop old process after autoupdate. Otherwise we create a new process // stop old process after autoupdate. Otherwise we create a new process
// after each update // after each update
@ -254,25 +264,9 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
} else if updateOutcome.UserMessage != "" { } else if updateOutcome.UserMessage != "" {
a.log.Warn().Msg(updateOutcome.UserMessage) a.log.Warn().Msg(updateOutcome.UserMessage)
} }
select {
case <-ctx.Done():
return ctx.Err()
case newConfigurable := <-a.updateConfigChan:
ticker.Stop()
a.configurable = newConfigurable
ticker = time.NewTicker(a.configurable.freq)
// Check if there is new version of cloudflared after receiving new AutoUpdaterConfigurable
case <-ticker.C:
}
} }
} }
// Update is the method to pass new AutoUpdaterConfigurable to a running AutoUpdater. It is safe to be called concurrently
func (a *AutoUpdater) Update(updateDisabled bool, newFreq time.Duration) {
a.updateConfigChan <- createUpdateConfig(updateDisabled, newFreq, a.log)
}
func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool { func isAutoupdateEnabled(log *zerolog.Logger, updateDisabled bool, updateFreq time.Duration) bool {
if !supportAutoUpdate(log) { if !supportAutoUpdate(log) {
return false return false

View File

@ -9,8 +9,14 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
) )
func init() {
Init(cliutil.GetBuildInfo("TEST", "TEST"))
}
func TestDisabledAutoUpdater(t *testing.T) { func TestDisabledAutoUpdater(t *testing.T) {
listeners := &gracenet.Net{} listeners := &gracenet.Net{}
log := zerolog.Nop() log := zerolog.Nop()

View File

@ -3,6 +3,7 @@ package updater
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"runtime" "runtime"
) )
@ -56,6 +57,9 @@ func (s *WorkersService) Check() (CheckResult, error) {
} }
req, err := http.NewRequest(http.MethodGet, s.url, nil) req, err := http.NewRequest(http.MethodGet, s.url, nil)
if err != nil {
return nil, err
}
q := req.URL.Query() q := req.URL.Query()
q.Add(OSKeyName, runtime.GOOS) q.Add(OSKeyName, runtime.GOOS)
q.Add(ArchitectureKeyName, runtime.GOARCH) q.Add(ArchitectureKeyName, runtime.GOARCH)
@ -76,6 +80,10 @@ func (s *WorkersService) Check() (CheckResult, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("unable to check for update: %d", resp.StatusCode)
}
var v VersionResponse var v VersionResponse
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return nil, err return nil, err

View File

@ -1,5 +1,4 @@
//go:build !windows //go:build !windows
// +build !windows
package updater package updater

View File

@ -3,7 +3,6 @@ package updater
import ( import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -11,11 +10,15 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"text/template" "text/template"
"time" "time"
"github.com/getsentry/sentry-go"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
) )
const ( const (
@ -27,9 +30,9 @@ const (
// start the service // start the service
// exit with code 0 if we've reached this point indicating success. // exit with code 0 if we've reached this point indicating success.
windowsUpdateCommandTemplate = `sc stop cloudflared >nul 2>&1 windowsUpdateCommandTemplate = `sc stop cloudflared >nul 2>&1
del "{{.OldPath}}"
rename "{{.TargetPath}}" {{.OldName}} rename "{{.TargetPath}}" {{.OldName}}
rename "{{.NewPath}}" {{.BinaryName}} rename "{{.NewPath}}" {{.BinaryName}}
del "{{.OldPath}}"
sc start cloudflared >nul 2>&1 sc start cloudflared >nul 2>&1
exit /b 0` exit /b 0`
batchFileName = "cfd_update.bat" batchFileName = "cfd_update.bat"
@ -86,8 +89,25 @@ func (v *WorkersVersion) Apply() error {
return err return err
} }
// check that the file is what is expected downloadSum, err := cliutil.FileChecksum(newFilePath)
if err := isValidChecksum(v.checksum, newFilePath); err != nil { if err != nil {
return err
}
// Check that the file downloaded matches what is expected.
if v.checksum != downloadSum {
return errors.New("checksum validation failed")
}
// Check if the currently running version has the same checksum
if downloadSum == buildInfo.Checksum {
// Currently running binary matches the downloaded binary so we have no reason to update. This is
// typically unexpected, as such we emit a sentry event.
localHub := sentry.CurrentHub().Clone()
err := errors.New("checksum validation matches currently running process")
localHub.CaptureException(err)
// Make sure to cleanup the new downloaded file since we aren't upgrading versions.
os.Remove(newFilePath)
return err return err
} }
@ -114,7 +134,7 @@ func (v *WorkersVersion) Apply() error {
if err := os.Rename(newFilePath, v.targetPath); err != nil { if err := os.Rename(newFilePath, v.targetPath); err != nil {
//attempt rollback //attempt rollback
os.Rename(oldFilePath, v.targetPath) _ = os.Rename(oldFilePath, v.targetPath)
return err return err
} }
os.Remove(oldFilePath) os.Remove(oldFilePath)
@ -161,7 +181,7 @@ func download(url, filepath string, isCompressed bool) error {
tr := tar.NewReader(gr) tr := tar.NewReader(gr)
// advance the reader pass the header, which will be the single binary file // advance the reader pass the header, which will be the single binary file
tr.Next() _, _ = tr.Next()
r = tr r = tr
} }
@ -178,7 +198,7 @@ func download(url, filepath string, isCompressed bool) error {
// isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped // isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped
func isCompressedFile(urlstring string) bool { func isCompressedFile(urlstring string) bool {
if strings.HasSuffix(urlstring, ".tgz") { if path.Ext(urlstring) == ".tgz" {
return true return true
} }
@ -186,28 +206,7 @@ func isCompressedFile(urlstring string) bool {
if err != nil { if err != nil {
return false return false
} }
return strings.HasSuffix(u.Path, ".tgz") return path.Ext(u.Path) == ".tgz"
}
// checks if the checksum in the json response matches the checksum of the file download
func isValidChecksum(checksum, filePath string) error {
f, err := os.Open(filePath)
if err != nil {
return err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return err
}
hash := fmt.Sprintf("%x", h.Sum(nil))
if checksum != hash {
return errors.New("checksum validation failed")
}
return nil
} }
// writeBatchFile writes a batch file out to disk // writeBatchFile writes a batch file out to disk
@ -250,7 +249,6 @@ func runWindowsBatch(batchFile string) error {
if exitError, ok := err.(*exec.ExitError); ok { if exitError, ok := err.(*exec.ExitError); ok {
return fmt.Errorf("Error during update : %s;", string(exitError.Stderr)) return fmt.Errorf("Error during update : %s;", string(exitError.Stderr))
} }
} }
return err return err
} }

View File

@ -1,5 +1,4 @@
//go:build windows //go:build windows
// +build windows
package main package main
@ -27,7 +26,7 @@ import (
const ( const (
windowsServiceName = "Cloudflared" windowsServiceName = "Cloudflared"
windowsServiceDescription = "Cloudflared agent" windowsServiceDescription = "Cloudflared agent"
windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/as-a-service/windows/" windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configure-tunnels/local-management/as-a-service/windows/"
recoverActionDelay = time.Second * 20 recoverActionDelay = time.Second * 20
failureCountResetPeriod = time.Hour * 24 failureCountResetPeriod = time.Hour * 24

View File

@ -1,9 +1,9 @@
# Requirements # Requirements
1. Python 3.7 or later with packages in the given `requirements.txt` 1. Python 3.10 or later with packages in the given `requirements.txt`
- E.g. with conda: - E.g. with venv:
- `conda create -n component-tests python=3.7` - `python3 -m venv ./.venv`
- `conda activate component-tests` - `source ./.venv/bin/activate`
- `pip3 install -r requirements.txt` - `python3 -m pip install -r requirements.txt`
2. Create a config yaml file, for example: 2. Create a config yaml file, for example:
``` ```

View File

@ -1,8 +1,8 @@
cloudflare==2.8.15 cloudflare==2.14.3
flaky==3.7.0 flaky==3.7.0
pytest==7.3.1 pytest==7.3.1
pytest-asyncio==0.21.0 pytest-asyncio==0.21.0
pyyaml==5.4.1 pyyaml==6.0.1
requests==2.28.2 requests==2.28.2
retrying==1.3.4 retrying==1.3.4
websockets==11.0.1 websockets==11.0.1

View File

@ -74,7 +74,7 @@ def delete_tunnel(config):
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def create_dns(config, hostname, type, content): def create_dns(config, hostname, type, content):
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN")) cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN"))
cf.zones.dns_records.post( cf.zones.dns_records.post(
config["zone_tag"], config["zone_tag"],
data={'name': hostname, 'type': type, 'content': content, 'proxied': True} data={'name': hostname, 'type': type, 'content': content, 'proxied': True}
@ -89,7 +89,7 @@ def create_named_dns(config, random_uuid):
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000) @retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def delete_dns(config, hostname): def delete_dns(config, hostname):
cf = CloudFlare.CloudFlare(debug=True, token=get_env("DNS_API_TOKEN")) cf = CloudFlare.CloudFlare(debug=False, token=get_env("DNS_API_TOKEN"))
zone_tag = config["zone_tag"] zone_tag = config["zone_tag"]
dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname}) dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname})
if len(dns_records) > 0: if len(dns_records) > 0:

View File

@ -36,17 +36,17 @@ class TestConfig:
_ = start_cloudflared(tmp_path, config, validate_args) _ = start_cloudflared(tmp_path, config, validate_args)
self.match_rule(tmp_path, config, self.match_rule(tmp_path, config,
"http://example.com/index.html", 1) "http://example.com/index.html", 0)
self.match_rule(tmp_path, config, self.match_rule(tmp_path, config,
"https://example.com/index.html", 1) "https://example.com/index.html", 0)
self.match_rule(tmp_path, config, self.match_rule(tmp_path, config,
"https://api.example.com/login", 2) "https://api.example.com/login", 1)
self.match_rule(tmp_path, config, self.match_rule(tmp_path, config,
"https://wss.example.com", 3) "https://wss.example.com", 2)
self.match_rule(tmp_path, config, self.match_rule(tmp_path, config,
"https://ssh.example.com", 4) "https://ssh.example.com", 3)
self.match_rule(tmp_path, config, self.match_rule(tmp_path, config,
"https://api.example.com", 5) "https://api.example.com", 4)
# This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. Note that rule number uses 1-based indexing # This is used to check that the command tunnel ingress url <url> matches rule number <rule_num>. Note that rule number uses 1-based indexing

View File

@ -55,7 +55,7 @@ class TestManagement:
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False) config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config) LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_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"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1) wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER) 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)
@ -76,7 +76,7 @@ class TestManagement:
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False) config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config) LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_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"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1) wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER) 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)
@ -97,7 +97,7 @@ class TestManagement:
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False) config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config) LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_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): 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) wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER) 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)

View File

@ -1,7 +1,6 @@
from util import LOGGER, nofips, start_cloudflared, wait_tunnel_ready from util import LOGGER, start_cloudflared, wait_tunnel_ready
@nofips
class TestPostQuantum: class TestPostQuantum:
def _extra_config(self): def _extra_config(self):
config = { config = {
@ -12,6 +11,11 @@ class TestPostQuantum:
def test_post_quantum(self, tmp_path, component_tests_config): def test_post_quantum(self, tmp_path, component_tests_config):
config = component_tests_config(self._extra_config()) config = component_tests_config(self._extra_config())
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--post-quantum"], new_process=True): with start_cloudflared(
wait_tunnel_ready(tunnel_url=config.get_url(), tmp_path,
require_min_connections=1) config,
cfd_pre_args=["tunnel", "--ha-connections", "1"],
cfd_args=["run", "--post-quantum"],
new_process=True,
):
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
from conftest import CfdModes from conftest import CfdModes
from constants import METRICS_PORT from constants import METRICS_PORT
import time
from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_url, send_requests from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_url, send_requests
class TestQuickTunnels: class TestQuickTunnels:
@ -9,6 +10,7 @@ class TestQuickTunnels:
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True): 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) wait_tunnel_ready(require_min_connections=1)
time.sleep(10)
url = get_quicktunnel_url() url = get_quicktunnel_url()
send_requests(url, 3, True) send_requests(url, 3, True)
@ -17,6 +19,7 @@ class TestQuickTunnels:
LOGGER.debug(config) 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): 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) wait_tunnel_ready(require_min_connections=1)
time.sleep(10)
url = get_quicktunnel_url() url = get_quicktunnel_url()
send_requests(url+"/ready", 3, True) send_requests(url+"/ready", 3, True)

View File

@ -47,7 +47,12 @@ class TestTail:
url = cfd_cli.get_management_wsurl("logs", config, config_path) url = cfd_cli.get_management_wsurl("logs", config, config_path)
async with connect(url, open_timeout=5, close_timeout=5) as websocket: async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# send start_streaming # send start_streaming
await websocket.send('{"type": "start_streaming"}') await websocket.send(json.dumps({
"type": "start_streaming",
"filters": {
"events": ["http"]
}
}))
# send some http requests to the tunnel to trigger some logs # send some http requests to the tunnel to trigger some logs
await generate_and_validate_http_events(websocket, config.get_url(), 10) await generate_and_validate_http_events(websocket, config.get_url(), 10)
# send stop_streaming # send stop_streaming
@ -99,7 +104,8 @@ class TestTail:
await websocket.send(json.dumps({ await websocket.send(json.dumps({
"type": "start_streaming", "type": "start_streaming",
"filters": { "filters": {
"sampling": 0.5 "sampling": 0.5,
"events": ["http"]
} }
})) }))
# don't expect any http logs # don't expect any http logs

View File

@ -45,9 +45,10 @@ class TestTermination:
with connected: with connected:
connected.wait(self.timeout) connected.wait(self.timeout)
# Send signal after the SSE connection is established # Send signal after the SSE connection is established
self.terminate_by_signal(cloudflared, signal) with self.within_grace_period():
self.wait_eyeball_thread( self.terminate_by_signal(cloudflared, signal)
in_flight_req, self.grace_period + self.timeout) self.wait_eyeball_thread(
in_flight_req, self.grace_period + self.timeout)
# test cloudflared terminates before grace period expires when all eyeball # test cloudflared terminates before grace period expires when all eyeball
# connections are drained # connections are drained
@ -66,7 +67,7 @@ class TestTermination:
with connected: with connected:
connected.wait(self.timeout) connected.wait(self.timeout)
with self.within_grace_period(): with self.within_grace_period(has_connection=False):
# Send signal after the SSE connection is established # Send signal after the SSE connection is established
self.terminate_by_signal(cloudflared, signal) self.terminate_by_signal(cloudflared, signal)
self.wait_eyeball_thread(in_flight_req, self.grace_period) self.wait_eyeball_thread(in_flight_req, self.grace_period)
@ -78,7 +79,7 @@ class TestTermination:
with start_cloudflared( with start_cloudflared(
tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared: tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared:
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
with self.within_grace_period(): with self.within_grace_period(has_connection=False):
self.terminate_by_signal(cloudflared, signal) self.terminate_by_signal(cloudflared, signal)
def terminate_by_signal(self, cloudflared, sig): def terminate_by_signal(self, cloudflared, sig):
@ -92,13 +93,21 @@ class TestTermination:
# Using this context asserts logic within the context is executed within grace period # Using this context asserts logic within the context is executed within grace period
@contextmanager @contextmanager
def within_grace_period(self): def within_grace_period(self, has_connection=True):
try: try:
start = time.time() start = time.time()
yield yield
finally: finally:
# If the request takes longer than the grace period then we need to wait at most the grace period.
# If the request fell within the grace period cloudflared can close earlier, but to ensure that it doesn't
# close immediately we add a minimum boundary. If cloudflared shutdown in less than 1s it's likely that
# it shutdown as soon as it received SIGINT. The only way cloudflared can close immediately is if it has no
# in-flight requests
minimum = 1 if has_connection else 0
duration = time.time() - start duration = time.time() - start
assert duration < self.grace_period # Here we truncate to ensure that we don't fail on minute differences like 10.1 instead of 10
assert minimum <= int(duration) <= self.grace_period
def stream_request(self, config, connected, early_terminate): def stream_request(self, config, connected, early_terminate):
expected_terminate_message = "502 Bad Gateway" expected_terminate_message = "502 Bad Gateway"

View File

@ -4,6 +4,7 @@ import platform
import subprocess import subprocess
from contextlib import contextmanager from contextlib import contextmanager
from time import sleep from time import sleep
import sys
import pytest import pytest
@ -14,8 +15,14 @@ from retrying import retry
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
LOGGER = logging.getLogger(__name__) def configure_logger():
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
logger.addHandler(handler)
return logger
LOGGER = configure_logger()
def select_platform(plat): def select_platform(plat):
return pytest.mark.skipif( return pytest.mark.skipif(

View File

@ -155,7 +155,7 @@ func FindOrCreateConfigPath() string {
// i.e. it fails if a user specifies both --url and --unix-socket // i.e. it fails if a user specifies both --url and --unix-socket
func ValidateUnixSocket(c *cli.Context) (string, error) { func ValidateUnixSocket(c *cli.Context) (string, error) {
if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) { if c.IsSet("unix-socket") && (c.IsSet("url") || c.NArg() > 0) {
return "", errors.New("--unix-socket must be used exclusivly.") return "", errors.New("--unix-socket must be used exclusively.")
} }
return c.String("unix-socket"), nil return c.String("unix-socket"), nil
} }
@ -205,6 +205,8 @@ type OriginRequestConfig struct {
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"` HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
// Hostname on the origin server certificate. // Hostname on the origin server certificate.
OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"` OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"`
// Auto configure the Hostname on the origin server certificate.
MatchSNIToHost *bool `yaml:"matchSNItoHost" json:"matchSNItoHost,omitempty"`
// Path to the CA for the certificate of your origin. // Path to the CA for the certificate of your origin.
// This option should be used only if your certificate is not signed by Cloudflare. // This option should be used only if your certificate is not signed by Cloudflare.
CAPool *string `yaml:"caPool" json:"caPool,omitempty"` CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
@ -257,8 +259,8 @@ type Configuration struct {
} }
type WarpRoutingConfig struct { type WarpRoutingConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"` ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
MaxActiveFlows *uint64 `yaml:"maxActiveFlows" json:"maxActiveFlows,omitempty"`
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"` TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
} }

View File

@ -23,7 +23,6 @@ func TestConfigFileSettings(t *testing.T) {
Service: "https://localhost:8001", Service: "https://localhost:8001",
} }
warpRouting = WarpRoutingConfig{ warpRouting = WarpRoutingConfig{
Enabled: true,
ConnectTimeout: &CustomDuration{Duration: 2 * time.Second}, ConnectTimeout: &CustomDuration{Duration: 2 * time.Second},
TCPKeepAlive: &CustomDuration{Duration: 10 * time.Second}, TCPKeepAlive: &CustomDuration{Duration: 10 * time.Second},
} }

View File

@ -26,24 +26,36 @@ const (
MaxGracePeriod = time.Minute * 3 MaxGracePeriod = time.Minute * 3
MaxConcurrentStreams = math.MaxUint32 MaxConcurrentStreams = math.MaxUint32
contentTypeHeader = "content-type" contentTypeHeader = "content-type"
sseContentType = "text/event-stream" contentLengthHeader = "content-length"
grpcContentType = "application/grpc" transferEncodingHeader = "transfer-encoding"
sseContentType = "text/event-stream"
grpcContentType = "application/grpc"
sseJsonContentType = "application/x-ndjson"
chunkTransferEncoding = "chunked"
) )
var ( var (
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols)) switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
flushableContentTypes = []string{sseContentType, grpcContentType} flushableContentTypes = []string{sseContentType, grpcContentType, sseJsonContentType}
) )
// TunnelConnection represents the connection to the edge.
// The Serve method is provided to allow clients to handle any errors from the connection encountered during
// processing of the connection. Cancelling of the context provided to Serve will close the connection.
type TunnelConnection interface {
Serve(ctx context.Context) error
}
type Orchestrator interface { type Orchestrator interface {
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
GetConfigJSON() ([]byte, error) GetConfigJSON() ([]byte, error)
GetOriginProxy() (OriginProxy, error) GetOriginProxy() (OriginProxy, error)
WarpRoutingEnabled() (enabled bool)
} }
type NamedTunnelProperties struct { type TunnelProperties struct {
Credentials Credentials Credentials Credentials
Client pogs.ClientInfo Client pogs.ClientInfo
QuickTunnelUrl string QuickTunnelUrl string
@ -54,6 +66,7 @@ type Credentials struct {
AccountTag string AccountTag string
TunnelSecret []byte TunnelSecret []byte
TunnelID uuid.UUID TunnelID uuid.UUID
Endpoint string
} }
func (c *Credentials) Auth() pogs.TunnelAuth { func (c *Credentials) Auth() pogs.TunnelAuth {
@ -68,13 +81,16 @@ type TunnelToken struct {
AccountTag string `json:"a"` AccountTag string `json:"a"`
TunnelSecret []byte `json:"s"` TunnelSecret []byte `json:"s"`
TunnelID uuid.UUID `json:"t"` TunnelID uuid.UUID `json:"t"`
Endpoint string `json:"e,omitempty"`
} }
func (t TunnelToken) Credentials() Credentials { func (t TunnelToken) Credentials() Credentials {
// nolint: gosimple
return Credentials{ return Credentials{
AccountTag: t.AccountTag, AccountTag: t.AccountTag,
TunnelSecret: t.TunnelSecret, TunnelSecret: t.TunnelSecret,
TunnelID: t.TunnelID, TunnelID: t.TunnelID,
Endpoint: t.Endpoint,
} }
} }
@ -264,6 +280,22 @@ type ConnectedFuse interface {
// Helper method to let the caller know what content-types should require a flush on every // Helper method to let the caller know what content-types should require a flush on every
// write to a ResponseWriter. // write to a ResponseWriter.
func shouldFlush(headers http.Header) bool { func shouldFlush(headers http.Header) bool {
// When doing Server Side Events (SSE), some frameworks don't respect the `Content-Type` header.
// Therefore, we need to rely on other ways to know whether we should flush on write or not. A good
// approach is to assume that responses without `Content-Length` or with `Transfer-Encoding: chunked`
// are streams, and therefore, should be flushed right away to the eyeball.
// References:
// - https://datatracker.ietf.org/doc/html/rfc7230#section-4.1
// - https://datatracker.ietf.org/doc/html/rfc9112#section-6.1
if contentLength := headers.Get(contentLengthHeader); contentLength == "" {
return true
}
if transferEncoding := headers.Get(transferEncodingHeader); transferEncoding != "" {
transferEncoding = strings.ToLower(transferEncoding)
if strings.Contains(transferEncoding, chunkTransferEncoding) {
return true
}
}
if contentType := headers.Get(contentTypeHeader); contentType != "" { if contentType := headers.Get(contentTypeHeader); contentType != "" {
contentType = strings.ToLower(contentType) contentType = strings.ToLower(contentType)
for _, c := range flushableContentTypes { for _, c := range flushableContentTypes {
@ -272,7 +304,6 @@ func shouldFlush(headers http.Header) bool {
} }
} }
} }
return false return false
} }

View File

@ -2,13 +2,19 @@ package connection
import ( import (
"context" "context"
"crypto/rand"
"fmt" "fmt"
"io" "io"
"math/rand" "math/big"
"net/http" "net/http"
"testing"
"time" "time"
pkgerrors "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/require"
cfdflow "github.com/cloudflare/cloudflared/flow"
"github.com/cloudflare/cloudflared/stream" "github.com/cloudflare/cloudflared/stream"
"github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tracing"
@ -77,7 +83,7 @@ func (moc *mockOriginProxy) ProxyHTTP(
return wsFlakyEndpoint(w, req) return wsFlakyEndpoint(w, req)
default: default:
originRespEndpoint(w, http.StatusNotFound, []byte("ws endpoint not found")) originRespEndpoint(w, http.StatusNotFound, []byte("ws endpoint not found"))
return fmt.Errorf("Unknwon websocket endpoint %s", req.URL.Path) return fmt.Errorf("unknown websocket endpoint %s", req.URL.Path)
} }
} }
switch req.URL.Path { switch req.URL.Path {
@ -95,7 +101,6 @@ func (moc *mockOriginProxy) ProxyHTTP(
originRespEndpoint(w, http.StatusNotFound, []byte("page not found")) originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
} }
return nil return nil
} }
func (moc *mockOriginProxy) ProxyTCP( func (moc *mockOriginProxy) ProxyTCP(
@ -103,6 +108,10 @@ func (moc *mockOriginProxy) ProxyTCP(
rwa ReadWriteAcker, rwa ReadWriteAcker,
r *TCPRequest, r *TCPRequest,
) error { ) error {
if r.CfTraceID == "flow-rate-limited" {
return pkgerrors.Wrap(cfdflow.ErrTooManyActiveFlows, "tcp flow rate limited")
}
return nil return nil
} }
@ -178,7 +187,8 @@ func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log) wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
closedAfter := time.Millisecond * time.Duration(rand.Intn(50)) rInt, _ := rand.Int(rand.Reader, big.NewInt(50))
closedAfter := time.Millisecond * time.Duration(rInt.Int64())
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)} originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
stream.Pipe(wsConn, originConn, &log) stream.Pipe(wsConn, originConn, &log)
cancel() cancel()
@ -201,3 +211,48 @@ func (mcf mockConnectedFuse) Connected() {}
func (mcf mockConnectedFuse) IsConnected() bool { func (mcf mockConnectedFuse) IsConnected() bool {
return true return true
} }
func TestShouldFlushHeaders(t *testing.T) {
tests := []struct {
headers map[string]string
shouldFlush bool
}{
{
headers: map[string]string{contentTypeHeader: "application/json", contentLengthHeader: "1"},
shouldFlush: false,
},
{
headers: map[string]string{contentTypeHeader: "text/html", contentLengthHeader: "1"},
shouldFlush: false,
},
{
headers: map[string]string{contentTypeHeader: "text/event-stream", contentLengthHeader: "1"},
shouldFlush: true,
},
{
headers: map[string]string{contentTypeHeader: "application/grpc", contentLengthHeader: "1"},
shouldFlush: true,
},
{
headers: map[string]string{contentTypeHeader: "application/x-ndjson", contentLengthHeader: "1"},
shouldFlush: true,
},
{
headers: map[string]string{contentTypeHeader: "application/json"},
shouldFlush: true,
},
{
headers: map[string]string{contentTypeHeader: "application/json", contentLengthHeader: "-1", transferEncodingHeader: "chunked"},
shouldFlush: true,
},
}
for _, test := range tests {
headers := http.Header{}
for k, v := range test.headers {
headers.Add(k, v)
}
require.Equal(t, test.shouldFlush, shouldFlush(headers))
}
}

View File

@ -6,25 +6,27 @@ import (
"net" "net"
"time" "time"
"github.com/rs/zerolog" "github.com/pkg/errors"
"github.com/cloudflare/cloudflared/management" "github.com/cloudflare/cloudflared/management"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
// RPCClientFunc derives a named tunnel rpc client that can then be used to register and unregister connections. // registerClient derives a named tunnel rpc client that can then be used to register and unregister connections.
type RPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient type registerClientFunc func(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient
type controlStream struct { type controlStream struct {
observer *Observer observer *Observer
connectedFuse ConnectedFuse connectedFuse ConnectedFuse
namedTunnelProperties *NamedTunnelProperties tunnelProperties *TunnelProperties
connIndex uint8 connIndex uint8
edgeAddress net.IP edgeAddress net.IP
protocol Protocol protocol Protocol
newRPCClientFunc RPCClientFunc registerClientFunc registerClientFunc
registerTimeout time.Duration
gracefulShutdownC <-chan struct{} gracefulShutdownC <-chan struct{}
gracePeriod time.Duration gracePeriod time.Duration
@ -34,7 +36,7 @@ type controlStream struct {
// ControlStreamHandler registers connections with origintunneld and initiates graceful shutdown. // ControlStreamHandler registers connections with origintunneld and initiates graceful shutdown.
type ControlStreamHandler interface { type ControlStreamHandler interface {
// ServeControlStream handles the control plane of the transport in the current goroutine calling this // ServeControlStream handles the control plane of the transport in the current goroutine calling this
ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *pogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error
// IsStopped tells whether the method above has finished // IsStopped tells whether the method above has finished
IsStopped() bool IsStopped() bool
} }
@ -47,80 +49,101 @@ type TunnelConfigJSONGetter interface {
func NewControlStream( func NewControlStream(
observer *Observer, observer *Observer,
connectedFuse ConnectedFuse, connectedFuse ConnectedFuse,
namedTunnelConfig *NamedTunnelProperties, tunnelProperties *TunnelProperties,
connIndex uint8, connIndex uint8,
edgeAddress net.IP, edgeAddress net.IP,
newRPCClientFunc RPCClientFunc, registerClientFunc registerClientFunc,
registerTimeout time.Duration,
gracefulShutdownC <-chan struct{}, gracefulShutdownC <-chan struct{},
gracePeriod time.Duration, gracePeriod time.Duration,
protocol Protocol, protocol Protocol,
) ControlStreamHandler { ) ControlStreamHandler {
if newRPCClientFunc == nil { if registerClientFunc == nil {
newRPCClientFunc = newRegistrationRPCClient registerClientFunc = tunnelrpc.NewRegistrationClient
} }
return &controlStream{ return &controlStream{
observer: observer, observer: observer,
connectedFuse: connectedFuse, connectedFuse: connectedFuse,
namedTunnelProperties: namedTunnelConfig, tunnelProperties: tunnelProperties,
newRPCClientFunc: newRPCClientFunc, registerClientFunc: registerClientFunc,
connIndex: connIndex, registerTimeout: registerTimeout,
edgeAddress: edgeAddress, connIndex: connIndex,
gracefulShutdownC: gracefulShutdownC, edgeAddress: edgeAddress,
gracePeriod: gracePeriod, gracefulShutdownC: gracefulShutdownC,
protocol: protocol, gracePeriod: gracePeriod,
protocol: protocol,
} }
} }
func (c *controlStream) ServeControlStream( func (c *controlStream) ServeControlStream(
ctx context.Context, ctx context.Context,
rw io.ReadWriteCloser, rw io.ReadWriteCloser,
connOptions *tunnelpogs.ConnectionOptions, connOptions *pogs.ConnectionOptions,
tunnelConfigGetter TunnelConfigJSONGetter, tunnelConfigGetter TunnelConfigJSONGetter,
) error { ) error {
rpcClient := c.newRPCClientFunc(ctx, rw, c.observer.log) registrationClient := c.registerClientFunc(ctx, rw, c.registerTimeout)
registrationDetails, err := rpcClient.RegisterConnection(ctx, c.namedTunnelProperties, connOptions, c.connIndex, c.edgeAddress, c.observer) registrationDetails, err := registrationClient.RegisterConnection(
ctx,
c.tunnelProperties.Credentials.Auth(),
c.tunnelProperties.Credentials.TunnelID,
connOptions,
c.connIndex,
c.edgeAddress)
if err != nil { if err != nil {
rpcClient.Close() defer registrationClient.Close()
return err if err.Error() == DuplicateConnectionError {
c.observer.metrics.regFail.WithLabelValues("dup_edge_conn", "registerConnection").Inc()
return errDuplicationConnection
}
c.observer.metrics.regFail.WithLabelValues("server_error", "registerConnection").Inc()
return serverRegistrationErrorFromRPC(err)
} }
c.observer.metrics.regSuccess.WithLabelValues("registerConnection").Inc()
c.observer.logConnected(registrationDetails.UUID, c.connIndex, registrationDetails.Location, c.edgeAddress, c.protocol) c.observer.logConnected(registrationDetails.UUID, c.connIndex, registrationDetails.Location, c.edgeAddress, c.protocol)
c.observer.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location) c.observer.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location, c.edgeAddress)
c.connectedFuse.Connected() c.connectedFuse.Connected()
// if conn index is 0 and tunnel is not remotely managed, then send local ingress rules configuration // if conn index is 0 and tunnel is not remotely managed, then send local ingress rules configuration
if c.connIndex == 0 && !registrationDetails.TunnelIsRemotelyManaged { if c.connIndex == 0 && !registrationDetails.TunnelIsRemotelyManaged {
if tunnelConfig, err := tunnelConfigGetter.GetConfigJSON(); err == nil { if tunnelConfig, err := tunnelConfigGetter.GetConfigJSON(); err == nil {
if err := rpcClient.SendLocalConfiguration(ctx, tunnelConfig, c.observer); err != nil { if err := registrationClient.SendLocalConfiguration(ctx, tunnelConfig); err != nil {
c.observer.metrics.localConfigMetrics.pushesErrors.Inc()
c.observer.log.Err(err).Msg("unable to send local configuration") c.observer.log.Err(err).Msg("unable to send local configuration")
} }
c.observer.metrics.localConfigMetrics.pushes.Inc()
} else { } else {
c.observer.log.Err(err).Msg("failed to obtain current configuration") c.observer.log.Err(err).Msg("failed to obtain current configuration")
} }
} }
c.waitForUnregister(ctx, rpcClient) return c.waitForUnregister(ctx, registrationClient)
return nil
} }
func (c *controlStream) waitForUnregister(ctx context.Context, rpcClient NamedTunnelRPCClient) { func (c *controlStream) waitForUnregister(ctx context.Context, registrationClient tunnelrpc.RegistrationClient) error {
// wait for connection termination or start of graceful shutdown // wait for connection termination or start of graceful shutdown
defer rpcClient.Close() defer registrationClient.Close()
var shutdownError error
select { select {
case <-ctx.Done(): case <-ctx.Done():
shutdownError = ctx.Err()
break break
case <-c.gracefulShutdownC: case <-c.gracefulShutdownC:
c.stoppedGracefully = true c.stoppedGracefully = true
} }
c.observer.sendUnregisteringEvent(c.connIndex) c.observer.sendUnregisteringEvent(c.connIndex)
rpcClient.GracefulShutdown(ctx, c.gracePeriod) err := registrationClient.GracefulShutdown(ctx, c.gracePeriod)
if err != nil {
return errors.Wrap(err, "Error shutting down control stream")
}
c.observer.log.Info(). c.observer.log.Info().
Int(management.EventTypeKey, int(management.Cloudflared)). Int(management.EventTypeKey, int(management.Cloudflared)).
Uint8(LogFieldConnIndex, c.connIndex). Uint8(LogFieldConnIndex, c.connIndex).
IPAddr(LogFieldIPAddress, c.edgeAddress). IPAddr(LogFieldIPAddress, c.edgeAddress).
Msg("Unregistered tunnel connection") Msg("Unregistered tunnel connection")
return shutdownError
} }
func (c *controlStream) IsStopped() bool { func (c *controlStream) IsStopped() bool {

View File

@ -2,7 +2,6 @@ package connection
import ( import (
"github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
@ -71,8 +70,6 @@ func isHandshakeErrRecoverable(err error, connIndex uint8, observer *Observer) b
switch err.(type) { switch err.(type) {
case edgediscovery.DialError: case edgediscovery.DialError:
log.Error().Msg("Connection unable to dial edge") log.Error().Msg("Connection unable to dial edge")
case h2mux.MuxerHandshakeError:
log.Error().Msg("Connection handshake with edge server failed")
default: default:
log.Error().Msg("Connection failed") log.Error().Msg("Connection failed")
return false return false

View File

@ -1,12 +1,15 @@
package connection package connection
import "net"
// Event is something that happened to a connection, e.g. disconnection or registration. // Event is something that happened to a connection, e.g. disconnection or registration.
type Event struct { type Event struct {
Index uint8 Index uint8
EventType Status EventType Status
Location string Location string
Protocol Protocol Protocol Protocol
URL string URL string
EdgeAddress net.IP
} }
// Status is the status of a connection. // Status is the status of a connection.

View File

@ -1,32 +0,0 @@
package connection
import (
"time"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/h2mux"
)
const (
muxerTimeout = 5 * time.Second
)
type MuxerConfig struct {
HeartbeatInterval time.Duration
MaxHeartbeats uint64
CompressionSetting h2mux.CompressionSetting
MetricsUpdateFreq time.Duration
}
func (mc *MuxerConfig) H2MuxerConfig(h h2mux.MuxedStreamHandler, log *zerolog.Logger) *h2mux.MuxerConfig {
return &h2mux.MuxerConfig{
Timeout: muxerTimeout,
Handler: h,
IsClient: true,
HeartbeatInterval: mc.HeartbeatInterval,
MaxHeartbeats: mc.MaxHeartbeats,
Log: log,
CompressionQuality: mc.CompressionSetting,
}
}

View File

@ -1,128 +0,0 @@
package connection
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/h2mux"
)
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
// to an HTTP/1 Request object destined for the local origin web service.
// This operation includes conversion of the pseudo-headers into their closest
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
for _, header := range h2 {
name := strings.ToLower(header.Name)
if !IsH2muxControlRequestHeader(name) {
continue
}
switch name {
case ":method":
h1.Method = header.Value
case ":scheme":
// noop - use the preexisting scheme from h1.URL
case ":authority":
// Otherwise the host header will be based on the origin URL
h1.Host = header.Value
case ":path":
// We don't want to be an "opinionated" proxy, so ideally we would use :path as-is.
// However, this HTTP/1 Request object belongs to the Go standard library,
// whose URL package makes some opinionated decisions about the encoding of
// URL characters: see the docs of https://godoc.org/net/url#URL,
// in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath,
// which is always used when computing url.URL.String(), whether we'd like it or not.
//
// Well, not *always*. We could circumvent this by using url.URL.Opaque. But
// that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque
// is treated differently when HTTP_PROXY is set!
// See https://github.com/golang/go/issues/5684#issuecomment-66080888
//
// This means we are subject to the behavior of net/url's function `shouldEscape`
// (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101
if header.Value == "*" {
h1.URL.Path = "*"
continue
}
// Due to the behavior of validation.ValidateUrl, h1.URL may
// already have a partial value, with or without a trailing slash.
base := h1.URL.String()
base = strings.TrimRight(base, "/")
// But we know :path begins with '/', because we handled '*' above - see RFC7540
requestURL, err := url.Parse(base + header.Value)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
}
h1.URL = requestURL
case "content-length":
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
if err != nil {
return fmt.Errorf("unparseable content length")
}
h1.ContentLength = contentLength
case RequestUserHeaders:
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
// Find and parse user headers serialized into a single one
userHeaders, err := DeserializeHeaders(header.Value)
if err != nil {
return errors.Wrap(err, "Unable to parse user headers")
}
for _, userHeader := range userHeaders {
h1.Header.Add(userHeader.Name, userHeader.Value)
}
default:
// All other control headers shall just be proxied transparently
h1.Header.Add(header.Name, header.Value)
}
}
return nil
}
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Header) {
h2 = []h2mux.Header{
{Name: ":status", Value: strconv.Itoa(status)},
}
userHeaders := make(http.Header, len(h1))
for header, values := range h1 {
h2name := strings.ToLower(header)
if h2name == "content-length" {
// This header has meaning in HTTP/2 and will be used by the edge,
// so it should be sent as an HTTP/2 response header.
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
} else if !IsH2muxControlResponseHeader(h2name) || IsWebsocketClientHeader(h2name) {
// User headers, on the other hand, must all be serialized so that
// HTTP/2 header validation won't be applied to HTTP/1 header values
userHeaders[header] = values
}
}
// Perform user header serialization and set them in the single header
h2 = append(h2, h2mux.Header{Name: ResponseUserHeaders, Value: SerializeHeaders(userHeaders)})
return h2
}
// IsH2muxControlRequestHeader is called in the direction of eyeball -> origin.
func IsH2muxControlRequestHeader(headerName string) bool {
return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket request headers
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-")
}
// IsH2muxControlResponseHeader is called in the direction of eyeball <- origin.
func IsH2muxControlResponseHeader(headerName string) bool {
return headerName == "content-length" ||
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-int-") ||
strings.HasPrefix(headerName, "cf-cloudflared-")
}

View File

@ -1,642 +0,0 @@
package connection
import (
"fmt"
"math/rand"
"net/http"
"net/url"
"reflect"
"regexp"
"strings"
"testing"
"testing/quick"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/h2mux"
)
type ByName []h2mux.Header
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
if a[i].Name == a[j].Name {
return a[i].Value < a[j].Value
}
return a[i].Name < a[j].Name
}
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockHeaders := http.Header{
"Mock header 1": {"Mock value 1"},
"Mock header 2": {"Mock value 2"},
}
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeaders, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func createSerializedHeaders(headersField string, headers http.Header) []h2mux.Header {
return []h2mux.Header{{
Name: headersField,
Value: SerializeHeaders(headers),
}}
}
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
emptyHeaders := make(http.Header)
headersConversionErr := H2RequestHeadersToH1Request(
[]h2mux.Header{{
Name: RequestUserHeaders,
Value: SerializeHeaders(emptyHeaders),
}},
request,
)
assert.True(t, reflect.DeepEqual(emptyHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "//bad_path/"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com//bad_path/", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "/?query=mock%20value"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: "/mock%20path"},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
}, request.Header)
assert.Equal(t, "http://example.com/mock%20path", request.URL.String())
assert.NoError(t, headersConversionErr)
}
func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
type testCase struct {
path string
want string
}
testCases := []testCase{
{
path: "",
want: "",
},
{
path: "/",
want: "/",
},
{
path: "//",
want: "//",
},
{
path: "/test",
want: "/test",
},
{
path: "//test",
want: "//test",
},
{
// https://github.com/cloudflare/cloudflared/issues/81
path: "//test/",
want: "//test/",
},
{
path: "/%2Ftest",
want: "/%2Ftest",
},
{
path: "//%20test",
want: "//%20test",
},
{
// https://github.com/cloudflare/cloudflared/issues/124
path: "/test?get=somthing%20a",
want: "/test?get=somthing%20a",
},
{
path: "/%20",
want: "/%20",
},
{
// stdlib's EscapedPath() will always percent-encode ' '
path: "/ ",
want: "/%20",
},
{
path: "/ a ",
want: "/%20a%20",
},
{
path: "/a%20b",
want: "/a%20b",
},
{
path: "/foo/bar;param?query#frag",
want: "/foo/bar;param?query#frag",
},
{
// stdlib's EscapedPath() will always percent-encode non-ASCII chars
path: "/a␠b",
want: "/a%E2%90%A0b",
},
{
path: "/a-umlaut-ä",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%C3%A4",
want: "/a-umlaut-%C3%A4",
},
{
path: "/a-umlaut-%c3%a4",
want: "/a-umlaut-%c3%a4",
},
{
// here the second '#' is treated as part of the fragment
path: "/a#b#c",
want: "/a#b%23c",
},
{
path: "/a#b␠c",
want: "/a#b%E2%90%A0c",
},
{
path: "/a#b%20c",
want: "/a#b%20c",
},
{
path: "/a#b c",
want: "/a#b%20c",
},
{
// stdlib's EscapedPath() will always percent-encode '\'
path: "/\\",
want: "/%5C",
},
{
path: "/a\\",
want: "/a%5C",
},
{
path: "/a,b.c.",
want: "/a,b.c.",
},
{
path: "/.",
want: "/.",
},
{
// stdlib's EscapedPath() will always percent-encode '`'
path: "/a`",
want: "/a%60",
},
{
path: "/a[0]",
want: "/a[0]",
},
{
path: "/?a[0]=5 &b[]=",
want: "/?a[0]=5 &b[]=",
},
{
path: "/?a=%22b%20%22",
want: "/?a=%22b%20%22",
},
}
for index, testCase := range testCases {
requestURL := "https://example.com"
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
assert.NoError(t, err)
mockRequestHeaders := []h2mux.Header{
{Name: ":path", Value: testCase.path},
{Name: RequestUserHeaders, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.NoError(t, headersConversionErr)
assert.Equal(t,
http.Header{
"Mock header": []string{"Mock value"},
},
request.Header)
assert.Equal(t,
"https://example.com"+testCase.want,
request.URL.String(),
"Failed URL index: %v %#v", index, testCase)
}
}
func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
config := &quick.Config{
Values: func(args []reflect.Value, rand *rand.Rand) {
args[0] = reflect.ValueOf(randomHTTP2Path(t, rand))
},
}
type testOrigin struct {
url string
expectedScheme string
expectedBasePath string
}
testOrigins := []testOrigin{
{
url: "http://origin.hostname.example.com:8080",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080",
},
{
url: "http://origin.hostname.example.com:8080/api",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "http://origin.hostname.example.com:8080/api/",
expectedScheme: "http",
expectedBasePath: "http://origin.hostname.example.com:8080/api",
},
{
url: "https://origin.hostname.example.com:8080/api",
expectedScheme: "https",
expectedBasePath: "https://origin.hostname.example.com:8080/api",
},
}
// use multiple schemes to demonstrate that the URL is based on the
// origin's scheme, not the :scheme header
for _, testScheme := range []string{"http", "https"} {
for _, testOrigin := range testOrigins {
assertion := func(testPath string) bool {
const expectedMethod = "POST"
const expectedHostname = "request.hostname.example.com"
h2 := []h2mux.Header{
{Name: ":method", Value: expectedMethod},
{Name: ":scheme", Value: testScheme},
{Name: ":authority", Value: expectedHostname},
{Name: ":path", Value: testPath},
{Name: RequestUserHeaders, Value: ""},
}
h1, err := http.NewRequest("GET", testOrigin.url, nil)
require.NoError(t, err)
err = H2RequestHeadersToH1Request(h2, h1)
return assert.NoError(t, err) &&
assert.Equal(t, expectedMethod, h1.Method) &&
assert.Equal(t, expectedHostname, h1.Host) &&
assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) &&
assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String())
}
err := quick.Check(assertion, config)
assert.NoError(t, err)
}
}
}
func randomASCIIPrintableChar(rand *rand.Rand) int {
// smallest printable ASCII char is 32, largest is 126
const startPrintable = 32
const endPrintable = 127
return startPrintable + rand.Intn(endPrintable-startPrintable)
}
// randomASCIIText generates an ASCII string, some of whose characters may be
// percent-encoded. Its "logical length" (ignoring percent-encoding) is
// between 1 and `maxLength`.
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
length := minLength + rand.Intn(maxLength)
var result strings.Builder
for i := 0; i < length; i++ {
c := randomASCIIPrintableChar(rand)
// 1/4 chance of using percent encoding when not necessary
if c == '%' || rand.Intn(4) == 0 {
result.WriteString(fmt.Sprintf("%%%02X", c))
} else {
result.WriteByte(byte(c))
}
}
return result.String()
}
// Calls `randomASCIIText` and ensures the result is a valid URL path,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
re, err := regexp.Compile("[^/;,]*")
require.NoError(t, err)
return "/" + re.ReplaceAllStringFunc(text, url.PathEscape)
}
// Calls `randomASCIIText` and ensures the result is a valid URL query,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Query(rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
return "?" + strings.ReplaceAll(text, "#", "%23")
}
// Calls `randomASCIIText` and ensures the result is a valid URL fragment,
// i.e. one that can pass unchanged through url.URL.String()
func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string {
text := randomASCIIText(rand, minLength, maxLength)
u, err := url.Parse("#" + text)
require.NoError(t, err)
return u.String()
}
// Assemble a random :path pseudoheader that is legal by Go stdlib standards
// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations)
func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
result := randomHTTP1Path(t, rand, 1, 64)
if rand.Intn(2) == 1 {
result += randomHTTP1Query(rand, 1, 32)
}
if rand.Intn(2) == 1 {
result += randomHTTP1Fragment(t, rand, 1, 16)
}
return result
}
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []h2mux.Header) {
for name, values := range headers {
for _, value := range values {
h2muxHeaders = append(h2muxHeaders, h2mux.Header{Name: name, Value: value})
}
}
return h2muxHeaders
}
func TestParseRequestHeaders(t *testing.T) {
mockUserHeadersToSerialize := http.Header{
"Mock-Header-One": {"1", "1.5"},
"Mock-Header-Two": {"2"},
"Mock-Header-Three": {"3"},
}
mockHeaders := []h2mux.Header{
{Name: "One", Value: "1"}, // will be dropped
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeaders, Value: SerializeHeaders(mockUserHeadersToSerialize)},
}
expectedHeaders := []h2mux.Header{
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: "Mock-Header-One", Value: "1"},
{Name: "Mock-Header-One", Value: "1.5"},
{Name: "Mock-Header-Two", Value: "2"},
{Name: "Mock-Header-Three", Value: "3"},
}
h1 := &http.Request{
Header: make(http.Header),
}
err := H2RequestHeadersToH1Request(mockHeaders, h1)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
}
func TestIsH2muxControlRequestHeader(t *testing.T) {
controlRequestHeaders := []string{
// Anything that begins with cf-
"cf-sample-header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
// Websocket request headers
"connection",
"upgrade",
}
for _, header := range controlRequestHeaders {
assert.True(t, IsH2muxControlRequestHeader(header))
}
}
func TestIsH2muxControlResponseHeader(t *testing.T) {
controlResponseHeaders := []string{
// Anything that begins with cf-int- or cf-cloudflared-
"cf-int-sample-header",
"cf-cloudflared-sample-header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
}
for _, header := range controlResponseHeaders {
assert.True(t, IsH2muxControlResponseHeader(header))
}
}
func TestIsNotH2muxControlRequestHeader(t *testing.T) {
notControlRequestHeaders := []string{
"mock-header",
"another-sample-header",
}
for _, header := range notControlRequestHeaders {
assert.False(t, IsH2muxControlRequestHeader(header))
}
}
func TestIsNotH2muxControlResponseHeader(t *testing.T) {
notControlResponseHeaders := []string{
"mock-header",
"another-sample-header",
"upgrade",
"connection",
"cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared-
}
for _, header := range notControlResponseHeaders {
assert.False(t, IsH2muxControlResponseHeader(header))
}
}
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
mockHeaders := http.Header{
"User-header-one": {""},
"User-header-two": {"1", "2"},
"cf-header": {"cf-value"},
"cf-int-header": {"cf-int-value"},
"cf-cloudflared-header": {"cf-cloudflared-value"},
"Content-Length": {"123"},
}
mockResponse := http.Response{
StatusCode: 200,
Header: mockHeaders,
}
headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
serializedHeadersIndex := -1
for i, header := range headers {
if header.Name == ResponseUserHeaders {
serializedHeadersIndex = i
break
}
}
assert.NotEqual(t, -1, serializedHeadersIndex)
actualControlHeaders := append(
headers[:serializedHeadersIndex],
headers[serializedHeadersIndex+1:]...,
)
expectedControlHeaders := []h2mux.Header{
{Name: ":status", Value: "200"},
{Name: "content-length", Value: "123"},
}
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
expectedUserHeaders := []h2mux.Header{
{Name: "User-header-one", Value: ""},
{Name: "User-header-two", Value: "1"},
{Name: "User-header-two", Value: "2"},
{Name: "cf-header", Value: "cf-value"},
}
assert.NoError(t, err)
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
}
// The purpose of this test is to check that our code and the http.Header
// implementation don't throw validation errors about header size
func TestHeaderSize(t *testing.T) {
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
largeHeaders := http.Header{
"User-header": {largeValue},
}
mockResponse := http.Response{
StatusCode: 200,
Header: largeHeaders,
}
serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.NoError(t, err)
for _, header := range serializedHeaders {
request.Header.Set(header.Name, header.Value)
}
for _, header := range serializedHeaders {
if header.Name != ResponseUserHeaders {
continue
}
deserializedHeaders, err := DeserializeHeaders(header.Value)
assert.NoError(t, err)
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
}
}
func randSeq(n int) string {
randomizer := rand.New(rand.NewSource(17))
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[randomizer.Intn(len(letters))]
}
return string(b)
}
func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
ser := "eC1mb3J3YXJkZWQtcHJvdG8:aHR0cHM;dXBncmFkZS1pbnNlY3VyZS1yZXF1ZXN0cw:MQ;YWNjZXB0LWxhbmd1YWdl:ZW4tVVMsZW47cT0wLjkscnU7cT0wLjg;YWNjZXB0LWVuY29kaW5n:Z3ppcA;eC1mb3J3YXJkZWQtZm9y:MTczLjI0NS42MC42;dXNlci1hZ2VudA:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTRfNikgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzg0LjAuNDE0Ny44OSBTYWZhcmkvNTM3LjM2;c2VjLWZldGNoLW1vZGU:bmF2aWdhdGU;Y2RuLWxvb3A:Y2xvdWRmbGFyZQ;c2VjLWZldGNoLWRlc3Q:ZG9jdW1lbnQ;c2VjLWZldGNoLXVzZXI:PzE;c2VjLWZldGNoLXNpdGU:bm9uZQ;Y29va2ll:X19jZmR1aWQ9ZGNkOWZjOGNjNWMxMzE0NTMyYTFkMjhlZDEyOWRhOTYwMTU2OTk1MTYzNDsgX19jZl9ibT1mYzY2MzMzYzAzZmM0MWFiZTZmOWEyYzI2ZDUwOTA0YzIxYzZhMTQ2LTE1OTU2MjIzNDEtMTgwMC1BZTVzS2pIU2NiWGVFM05mMUhrTlNQMG1tMHBLc2pQWkloVnM1Z2g1SkNHQkFhS1UxVDB2b003alBGN3FjMHVSR2NjZGcrWHdhL1EzbTJhQzdDVU4xZ2M9;YWNjZXB0:dGV4dC9odG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCxhcHBsaWNhdGlvbi94bWw7cT0wLjksaW1hZ2Uvd2VicCxpbWFnZS9hcG5nLCovKjtxPTAuOCxhcHBsaWNhdGlvbi9zaWduZWQtZXhjaGFuZ2U7dj1iMztxPTAuOQ"
h2, _ := DeserializeHeaders(ser)
h1 := make(http.Header)
for _, header := range h2 {
h1.Add(header.Name, header.Value)
}
h1.Add("Content-Length", "200")
h1.Add("Cf-Something", "Else")
h1.Add("Upgrade", "websocket")
h1resp := &http.Response{
StatusCode: 200,
Header: h1,
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
}
}

View File

@ -7,33 +7,40 @@ import (
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/cloudflare/cloudflared/h2mux"
) )
var ( var (
// h2mux-style special headers // internal special headers
RequestUserHeaders = "cf-cloudflared-request-headers" RequestUserHeaders = "cf-cloudflared-request-headers"
ResponseUserHeaders = "cf-cloudflared-response-headers" ResponseUserHeaders = "cf-cloudflared-response-headers"
ResponseMetaHeader = "cf-cloudflared-response-meta" ResponseMetaHeader = "cf-cloudflared-response-meta"
// h2mux-style special headers // internal special headers
CanonicalResponseUserHeaders = http.CanonicalHeaderKey(ResponseUserHeaders) CanonicalResponseUserHeaders = http.CanonicalHeaderKey(ResponseUserHeaders)
CanonicalResponseMetaHeader = http.CanonicalHeaderKey(ResponseMetaHeader) CanonicalResponseMetaHeader = http.CanonicalHeaderKey(ResponseMetaHeader)
) )
var ( var (
// pre-generate possible values for res // pre-generate possible values for res
responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared") responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared", false)
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin") responseMetaHeaderCfdFlowRateLimited = mustInitRespMetaHeader("cloudflared", true)
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin", false)
) )
type responseMetaHeader struct { // HTTPHeader is a custom header struct that expects only ever one value for the header.
Source string `json:"src"` // This structure is used to serialize the headers and attach them to the HTTP2 request when proxying.
type HTTPHeader struct {
Name string
Value string
} }
func mustInitRespMetaHeader(src string) string { type responseMetaHeader struct {
header, err := json.Marshal(responseMetaHeader{Source: src}) Source string `json:"src"`
FlowRateLimited bool `json:"flow_rate_limited,omitempty"`
}
func mustInitRespMetaHeader(src string, flowRateLimited bool) string {
header, err := json.Marshal(responseMetaHeader{Source: src, FlowRateLimited: flowRateLimited})
if err != nil { if err != nil {
panic(fmt.Sprintf("Failed to serialize response meta header = %s, err: %v", src, err)) panic(fmt.Sprintf("Failed to serialize response meta header = %s, err: %v", src, err))
} }
@ -104,10 +111,10 @@ func SerializeHeaders(h1Headers http.Header) string {
} }
// Deserialize headers serialized by `SerializeHeader` // Deserialize headers serialized by `SerializeHeader`
func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) { func DeserializeHeaders(serializedHeaders string) ([]HTTPHeader, error) {
const unableToDeserializeErr = "Unable to deserialize headers" const unableToDeserializeErr = "Unable to deserialize headers"
var deserialized []h2mux.Header deserialized := make([]HTTPHeader, 0)
for _, serializedPair := range strings.Split(serializedHeaders, ";") { for _, serializedPair := range strings.Split(serializedHeaders, ";") {
if len(serializedPair) == 0 { if len(serializedPair) == 0 {
continue continue
@ -130,7 +137,7 @@ func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) {
return nil, errors.Wrap(err, unableToDeserializeErr) return nil, errors.Wrap(err, unableToDeserializeErr)
} }
deserialized = append(deserialized, h2mux.Header{ deserialized = append(deserialized, HTTPHeader{
Name: string(deserializedName), Name: string(deserializedName),
Value: string(deserializedValue), Value: string(deserializedValue),
}) })

View File

@ -46,18 +46,40 @@ func TestSerializeHeaders(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 13, len(deserializedHeaders)) assert.Equal(t, 13, len(deserializedHeaders))
h2muxExpectedHeaders := stdlibHeaderToH2muxHeader(mockHeaders) expectedHeaders := headerToReqHeader(mockHeaders)
sort.Sort(ByName(deserializedHeaders)) sort.Sort(ByName(deserializedHeaders))
sort.Sort(ByName(h2muxExpectedHeaders)) sort.Sort(ByName(expectedHeaders))
assert.True( assert.True(
t, t,
reflect.DeepEqual(h2muxExpectedHeaders, deserializedHeaders), reflect.DeepEqual(expectedHeaders, deserializedHeaders),
fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, h2muxExpectedHeaders), fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, expectedHeaders),
) )
} }
type ByName []HTTPHeader
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
if a[i].Name == a[j].Name {
return a[i].Value < a[j].Value
}
return a[i].Name < a[j].Name
}
func headerToReqHeader(headers http.Header) (reqHeaders []HTTPHeader) {
for name, values := range headers {
for _, value := range values {
reqHeaders = append(reqHeaders, HTTPHeader{Name: name, Value: value})
}
}
return reqHeaders
}
func TestSerializeNoHeaders(t *testing.T) { func TestSerializeNoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -16,8 +16,10 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"golang.org/x/net/http2" "golang.org/x/net/http2"
cfdflow "github.com/cloudflare/cloudflared/flow"
"github.com/cloudflare/cloudflared/tracing" "github.com/cloudflare/cloudflared/tracing"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
// note: these constants are exported so we can reuse them in the edge-side code // note: these constants are exported so we can reuse them in the edge-side code
@ -37,11 +39,9 @@ type HTTP2Connection struct {
conn net.Conn conn net.Conn
server *http2.Server server *http2.Server
orchestrator Orchestrator orchestrator Orchestrator
connOptions *tunnelpogs.ConnectionOptions connOptions *pogs.ConnectionOptions
observer *Observer observer *Observer
connIndex uint8 connIndex uint8
// newRPCClientFunc allows us to mock RPCs during testing
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
log *zerolog.Logger log *zerolog.Logger
activeRequestsWG sync.WaitGroup activeRequestsWG sync.WaitGroup
@ -54,7 +54,7 @@ type HTTP2Connection struct {
func NewHTTP2Connection( func NewHTTP2Connection(
conn net.Conn, conn net.Conn,
orchestrator Orchestrator, orchestrator Orchestrator,
connOptions *tunnelpogs.ConnectionOptions, connOptions *pogs.ConnectionOptions,
observer *Observer, observer *Observer,
connIndex uint8, connIndex uint8,
controlStreamHandler ControlStreamHandler, controlStreamHandler ControlStreamHandler,
@ -69,7 +69,6 @@ func NewHTTP2Connection(
connOptions: connOptions, connOptions: connOptions,
observer: observer, observer: observer,
connIndex: connIndex, connIndex: connIndex,
newRPCClientFunc: newRegistrationRPCClient,
controlStreamHandler: controlStreamHandler, controlStreamHandler: controlStreamHandler,
log: log, log: log,
} }
@ -159,7 +158,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.log.Error().Err(requestErr).Msg("failed to serve incoming request") c.log.Error().Err(requestErr).Msg("failed to serve incoming request")
// WriteErrorResponse will return false if status was already written. we need to abort handler. // WriteErrorResponse will return false if status was already written. we need to abort handler.
if !respWriter.WriteErrorResponse() { if !respWriter.WriteErrorResponse(requestErr) {
c.log.Debug().Msg("Handler aborted due to failure to write error response after status already sent") c.log.Debug().Msg("Handler aborted due to failure to write error response after status already sent")
panic(http.ErrAbortHandler) panic(http.ErrAbortHandler)
} }
@ -212,8 +211,9 @@ func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type, l
w: w, w: w,
log: log, log: log,
} }
respWriter.WriteErrorResponse() err := fmt.Errorf("%T doesn't implement http.Flusher", w)
return nil, fmt.Errorf("%T doesn't implement http.Flusher", w) respWriter.WriteErrorResponse(err)
return nil, err
} }
return &http2RespWriter{ return &http2RespWriter{
@ -298,7 +298,7 @@ func (rp *http2RespWriter) WriteHeader(status int) {
rp.log.Warn().Msg("WriteHeader after hijack") rp.log.Warn().Msg("WriteHeader after hijack")
return return
} }
rp.WriteRespHeaders(status, rp.respHeaders) _ = rp.WriteRespHeaders(status, rp.respHeaders)
} }
func (rp *http2RespWriter) hijacked() bool { func (rp *http2RespWriter) hijacked() bool {
@ -331,12 +331,16 @@ func (rp *http2RespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return conn, readWriter, nil return conn, readWriter, nil
} }
func (rp *http2RespWriter) WriteErrorResponse() bool { func (rp *http2RespWriter) WriteErrorResponse(err error) bool {
if rp.statusWritten { if rp.statusWritten {
return false return false
} }
rp.setResponseMetaHeader(responseMetaHeaderCfd) if errors.Is(err, cfdflow.ErrTooManyActiveFlows) {
rp.setResponseMetaHeader(responseMetaHeaderCfdFlowRateLimited)
} else {
rp.setResponseMetaHeader(responseMetaHeaderCfd)
}
rp.w.WriteHeader(http.StatusBadGateway) rp.w.WriteHeader(http.StatusBadGateway)
rp.statusWritten = true rp.statusWritten = true
@ -388,8 +392,7 @@ func determineHTTP2Type(r *http.Request) Type {
func handleMissingRequestParts(connType Type, r *http.Request) { func handleMissingRequestParts(connType Type, r *http.Request) {
if connType == TypeHTTP { if connType == TypeHTTP {
// http library has no guarantees that we receive a filled URL. If not, then we fill it, as we reuse the request // http library has no guarantees that we receive a filled URL. If not, then we fill it, as we reuse the request
// for proxying. We use the same values as we used to in h2mux. For proxying they should not matter since we // for proxying. For proxying they should not matter since we control the dialer on every egress proxied.
// control the dialer on every egress proxied.
if len(r.URL.Scheme) == 0 { if len(r.URL.Scheme) == 0 {
r.URL.Scheme = "http" r.URL.Scheme = "http"
} }

View File

@ -20,8 +20,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"github.com/cloudflare/cloudflared/tracing"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs" "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
var ( var (
@ -36,10 +38,11 @@ func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelProperties{}, &TunnelProperties{},
connIndex, connIndex,
nil, nil,
nil, nil,
1*time.Second,
nil, nil,
1*time.Second, 1*time.Second,
HTTP2, HTTP2,
@ -64,19 +67,18 @@ func TestHTTP2ConfigurationSet(t *testing.T) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
http2Conn.Serve(ctx) _ = http2Conn.Serve(ctx)
}() }()
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
require.NoError(t, err) require.NoError(t, err)
endpoint := fmt.Sprintf("http://localhost:8080/ok")
reqBody := []byte(`{ reqBody := []byte(`{
"version": 2, "version": 2,
"config": {"warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}} "config": {"warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}}
`) `)
reader := bytes.NewReader(reqBody) reader := bytes.NewReader(reqBody)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, reader) req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://localhost:8080/ok", reader)
require.NoError(t, err) require.NoError(t, err)
req.Header.Set(InternalUpgradeHeader, ConfigurationUpdate) req.Header.Set(InternalUpgradeHeader, ConfigurationUpdate)
@ -84,11 +86,11 @@ func TestHTTP2ConfigurationSet(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, http.StatusOK, resp.StatusCode)
bdy, err := io.ReadAll(resp.Body) bdy, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy)) assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy))
cancel() cancel()
wg.Wait() wg.Wait()
} }
func TestServeHTTP(t *testing.T) { func TestServeHTTP(t *testing.T) {
@ -133,7 +135,7 @@ func TestServeHTTP(t *testing.T) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
http2Conn.Serve(ctx) _ = http2Conn.Serve(ctx)
}() }()
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn) edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
@ -152,6 +154,7 @@ func TestServeHTTP(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, test.expectedBody, respBody) require.Equal(t, test.expectedBody, respBody)
} }
_ = resp.Body.Close()
if test.isProxyError { if test.isProxyError {
require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(ResponseMetaHeader)) require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(ResponseMetaHeader))
} else { } else {
@ -168,31 +171,32 @@ type mockNamedTunnelRPCClient struct {
unregistered chan struct{} unregistered chan struct{}
} }
func (mc mockNamedTunnelRPCClient) SendLocalConfiguration(c context.Context, config []byte, observer *Observer) error { func (mc mockNamedTunnelRPCClient) SendLocalConfiguration(c context.Context, config []byte) error {
return nil return nil
} }
func (mc mockNamedTunnelRPCClient) RegisterConnection( func (mc mockNamedTunnelRPCClient) RegisterConnection(
c context.Context, ctx context.Context,
properties *NamedTunnelProperties, auth pogs.TunnelAuth,
options *tunnelpogs.ConnectionOptions, tunnelID uuid.UUID,
options *pogs.ConnectionOptions,
connIndex uint8, connIndex uint8,
edgeAddress net.IP, edgeAddress net.IP,
observer *Observer, ) (*pogs.ConnectionDetails, error) {
) (*tunnelpogs.ConnectionDetails, error) {
if mc.shouldFail != nil { if mc.shouldFail != nil {
return nil, mc.shouldFail return nil, mc.shouldFail
} }
close(mc.registered) close(mc.registered)
return &tunnelpogs.ConnectionDetails{ return &pogs.ConnectionDetails{
Location: "LIS", Location: "LIS",
UUID: uuid.New(), UUID: uuid.New(),
TunnelIsRemotelyManaged: false, TunnelIsRemotelyManaged: false,
}, nil }, nil
} }
func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) { func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) error {
close(mc.unregistered) close(mc.unregistered)
return nil
} }
func (mockNamedTunnelRPCClient) Close() {} func (mockNamedTunnelRPCClient) Close() {}
@ -203,8 +207,8 @@ type mockRPCClientFactory struct {
unregistered chan struct{} unregistered chan struct{}
} }
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient { func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient {
return mockNamedTunnelRPCClient{ return &mockNamedTunnelRPCClient{
shouldFail: mf.shouldFail, shouldFail: mf.shouldFail,
registered: mf.registered, registered: mf.registered,
unregistered: mf.unregistered, unregistered: mf.unregistered,
@ -279,10 +283,11 @@ func TestServeWS(t *testing.T) {
respBody, err := wsutil.ReadServerBinary(respWriter.RespBody()) respBody, err := wsutil.ReadServerBinary(respWriter.RespBody())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody))) require.Equal(t, data, respBody, "expect %s, got %s", string(data), string(respBody))
cancel() cancel()
resp := respWriter.Result() resp := respWriter.Result()
defer resp.Body.Close()
// http2RespWriter should rewrite status 101 to 200 // http2RespWriter should rewrite status 101 to 200
require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader)) require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader))
@ -302,7 +307,7 @@ func TestNoWriteAfterServeHTTPReturns(t *testing.T) {
serverDone := make(chan struct{}) serverDone := make(chan struct{})
go func() { go func() {
defer close(serverDone) defer close(serverDone)
cfdHTTP2Conn.Serve(ctx) _ = cfdHTTP2Conn.Serve(ctx)
}() }()
edgeTransport := http2.Transport{} edgeTransport := http2.Transport{}
@ -317,13 +322,16 @@ func TestNoWriteAfterServeHTTPReturns(t *testing.T) {
readPipe, writePipe := io.Pipe() readPipe, writePipe := io.Pipe()
reqCtx, reqCancel := context.WithCancel(ctx) reqCtx, reqCancel := context.WithCancel(ctx)
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, "http://localhost:8080/ws/flaky", readPipe) req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, "http://localhost:8080/ws/flaky", readPipe)
require.NoError(t, err) assert.NoError(t, err)
req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade) req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade)
resp, err := edgeHTTP2Conn.RoundTrip(req) resp, err := edgeHTTP2Conn.RoundTrip(req)
require.NoError(t, err) assert.NoError(t, err)
_ = resp.Body.Close()
// http2RespWriter should rewrite status 101 to 200 // http2RespWriter should rewrite status 101 to 200
require.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, http.StatusOK, resp.StatusCode)
wg.Add(1) wg.Add(1)
go func() { go func() {
@ -360,10 +368,11 @@ func TestServeControlStream(t *testing.T) {
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelProperties{}, &TunnelProperties{},
1, 1,
nil, nil,
rpcClientFactory.newMockRPCClient, rpcClientFactory.newMockRPCClient,
1*time.Second,
nil, nil,
1*time.Second, 1*time.Second,
HTTP2, HTTP2,
@ -375,7 +384,7 @@ func TestServeControlStream(t *testing.T) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
http2Conn.Serve(ctx) _ = http2Conn.Serve(ctx)
}() }()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
@ -388,7 +397,8 @@ func TestServeControlStream(t *testing.T) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
edgeHTTP2Conn.RoundTrip(req) // nolint: bodyclose
_, _ = edgeHTTP2Conn.RoundTrip(req)
}() }()
<-rpcClientFactory.registered <-rpcClientFactory.registered
@ -412,10 +422,11 @@ func TestFailRegistration(t *testing.T) {
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelProperties{}, &TunnelProperties{},
http2Conn.connIndex, http2Conn.connIndex,
nil, nil,
rpcClientFactory.newMockRPCClient, rpcClientFactory.newMockRPCClient,
1*time.Second,
nil, nil,
1*time.Second, 1*time.Second,
HTTP2, HTTP2,
@ -427,7 +438,7 @@ func TestFailRegistration(t *testing.T) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
http2Conn.Serve(ctx) _ = http2Conn.Serve(ctx)
}() }()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
@ -438,9 +449,10 @@ func TestFailRegistration(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
resp, err := edgeHTTP2Conn.RoundTrip(req) resp, err := edgeHTTP2Conn.RoundTrip(req)
require.NoError(t, err) require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadGateway, resp.StatusCode) require.Equal(t, http.StatusBadGateway, resp.StatusCode)
assert.NotNil(t, http2Conn.controlStreamErr) require.Error(t, http2Conn.controlStreamErr)
cancel() cancel()
wg.Wait() wg.Wait()
} }
@ -460,10 +472,11 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelProperties{}, &TunnelProperties{},
http2Conn.connIndex, http2Conn.connIndex,
nil, nil,
rpcClientFactory.newMockRPCClient, rpcClientFactory.newMockRPCClient,
1*time.Second,
shutdownC, shutdownC,
1*time.Second, 1*time.Second,
HTTP2, HTTP2,
@ -476,7 +489,7 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
http2Conn.Serve(ctx) _ = http2Conn.Serve(ctx)
}() }()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
@ -489,6 +502,7 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
// nolint: bodyclose
_, _ = edgeHTTP2Conn.RoundTrip(req) _, _ = edgeHTTP2Conn.RoundTrip(req)
}() }()
@ -519,6 +533,36 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
}) })
} }
func TestServeTCP_RateLimited(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
http2Conn, edgeConn := newTestHTTP2Connection()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = http2Conn.Serve(ctx)
}()
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
require.NoError(t, err)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080", nil)
require.NoError(t, err)
req.Header.Set(InternalTCPProxySrcHeader, "tcp")
req.Header.Set(tracing.TracerContextName, "flow-rate-limited")
resp, err := edgeHTTP2Conn.RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadGateway, resp.StatusCode)
require.Equal(t, responseMetaHeaderCfdFlowRateLimited, resp.Header.Get(ResponseMetaHeader))
cancel()
wg.Wait()
}
func benchmarkServeHTTP(b *testing.B, test testRequest) { func benchmarkServeHTTP(b *testing.B, test testRequest) {
http2Conn, edgeConn := newTestHTTP2Connection() http2Conn, edgeConn := newTestHTTP2Connection()
@ -527,7 +571,7 @@ func benchmarkServeHTTP(b *testing.B, test testRequest) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
http2Conn.Serve(ctx) _ = http2Conn.Serve(ctx)
}() }()
endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint) endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint)

View File

@ -2,11 +2,8 @@ package connection
import ( import (
"sync" "sync"
"time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/cloudflare/cloudflared/h2mux"
) )
const ( const (
@ -16,34 +13,12 @@ const (
configSubsystem = "config" configSubsystem = "config"
) )
type muxerMetrics struct {
rtt *prometheus.GaugeVec
rttMin *prometheus.GaugeVec
rttMax *prometheus.GaugeVec
receiveWindowAve *prometheus.GaugeVec
sendWindowAve *prometheus.GaugeVec
receiveWindowMin *prometheus.GaugeVec
receiveWindowMax *prometheus.GaugeVec
sendWindowMin *prometheus.GaugeVec
sendWindowMax *prometheus.GaugeVec
inBoundRateCurr *prometheus.GaugeVec
inBoundRateMin *prometheus.GaugeVec
inBoundRateMax *prometheus.GaugeVec
outBoundRateCurr *prometheus.GaugeVec
outBoundRateMin *prometheus.GaugeVec
outBoundRateMax *prometheus.GaugeVec
compBytesBefore *prometheus.GaugeVec
compBytesAfter *prometheus.GaugeVec
compRateAve *prometheus.GaugeVec
}
type localConfigMetrics struct { type localConfigMetrics struct {
pushes prometheus.Counter pushes prometheus.Counter
pushesErrors prometheus.Counter pushesErrors prometheus.Counter
} }
type tunnelMetrics struct { type tunnelMetrics struct {
timerRetries prometheus.Gauge
serverLocations *prometheus.GaugeVec serverLocations *prometheus.GaugeVec
// locationLock is a mutex for oldServerLocations // locationLock is a mutex for oldServerLocations
locationLock sync.Mutex locationLock sync.Mutex
@ -54,7 +29,6 @@ type tunnelMetrics struct {
regFail *prometheus.CounterVec regFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec rpcFail *prometheus.CounterVec
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA tunnelsHA tunnelsForHA
userHostnamesCounts *prometheus.CounterVec userHostnamesCounts *prometheus.CounterVec
@ -92,252 +66,6 @@ func newLocalConfigMetrics() *localConfigMetrics {
} }
} }
func newMuxerMetrics() *muxerMetrics {
rtt := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "rtt",
Help: "Round-trip time in millisecond",
},
[]string{"connection_id"},
)
prometheus.MustRegister(rtt)
rttMin := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "rtt_min",
Help: "Shortest round-trip time in millisecond",
},
[]string{"connection_id"},
)
prometheus.MustRegister(rttMin)
rttMax := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "rtt_max",
Help: "Longest round-trip time in millisecond",
},
[]string{"connection_id"},
)
prometheus.MustRegister(rttMax)
receiveWindowAve := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "receive_window_ave",
Help: "Average receive window size in bytes",
},
[]string{"connection_id"},
)
prometheus.MustRegister(receiveWindowAve)
sendWindowAve := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "send_window_ave",
Help: "Average send window size in bytes",
},
[]string{"connection_id"},
)
prometheus.MustRegister(sendWindowAve)
receiveWindowMin := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "receive_window_min",
Help: "Smallest receive window size in bytes",
},
[]string{"connection_id"},
)
prometheus.MustRegister(receiveWindowMin)
receiveWindowMax := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "receive_window_max",
Help: "Largest receive window size in bytes",
},
[]string{"connection_id"},
)
prometheus.MustRegister(receiveWindowMax)
sendWindowMin := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "send_window_min",
Help: "Smallest send window size in bytes",
},
[]string{"connection_id"},
)
prometheus.MustRegister(sendWindowMin)
sendWindowMax := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "send_window_max",
Help: "Largest send window size in bytes",
},
[]string{"connection_id"},
)
prometheus.MustRegister(sendWindowMax)
inBoundRateCurr := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "inbound_bytes_per_sec_curr",
Help: "Current inbounding bytes per second, 0 if there is no incoming connection",
},
[]string{"connection_id"},
)
prometheus.MustRegister(inBoundRateCurr)
inBoundRateMin := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "inbound_bytes_per_sec_min",
Help: "Minimum non-zero inbounding bytes per second",
},
[]string{"connection_id"},
)
prometheus.MustRegister(inBoundRateMin)
inBoundRateMax := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "inbound_bytes_per_sec_max",
Help: "Maximum inbounding bytes per second",
},
[]string{"connection_id"},
)
prometheus.MustRegister(inBoundRateMax)
outBoundRateCurr := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "outbound_bytes_per_sec_curr",
Help: "Current outbounding bytes per second, 0 if there is no outgoing traffic",
},
[]string{"connection_id"},
)
prometheus.MustRegister(outBoundRateCurr)
outBoundRateMin := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "outbound_bytes_per_sec_min",
Help: "Minimum non-zero outbounding bytes per second",
},
[]string{"connection_id"},
)
prometheus.MustRegister(outBoundRateMin)
outBoundRateMax := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "outbound_bytes_per_sec_max",
Help: "Maximum outbounding bytes per second",
},
[]string{"connection_id"},
)
prometheus.MustRegister(outBoundRateMax)
compBytesBefore := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "comp_bytes_before",
Help: "Bytes sent via cross-stream compression, pre compression",
},
[]string{"connection_id"},
)
prometheus.MustRegister(compBytesBefore)
compBytesAfter := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "comp_bytes_after",
Help: "Bytes sent via cross-stream compression, post compression",
},
[]string{"connection_id"},
)
prometheus.MustRegister(compBytesAfter)
compRateAve := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: muxerSubsystem,
Name: "comp_rate_ave",
Help: "Average outbound cross-stream compression ratio",
},
[]string{"connection_id"},
)
prometheus.MustRegister(compRateAve)
return &muxerMetrics{
rtt: rtt,
rttMin: rttMin,
rttMax: rttMax,
receiveWindowAve: receiveWindowAve,
sendWindowAve: sendWindowAve,
receiveWindowMin: receiveWindowMin,
receiveWindowMax: receiveWindowMax,
sendWindowMin: sendWindowMin,
sendWindowMax: sendWindowMax,
inBoundRateCurr: inBoundRateCurr,
inBoundRateMin: inBoundRateMin,
inBoundRateMax: inBoundRateMax,
outBoundRateCurr: outBoundRateCurr,
outBoundRateMin: outBoundRateMin,
outBoundRateMax: outBoundRateMax,
compBytesBefore: compBytesBefore,
compBytesAfter: compBytesAfter,
compRateAve: compRateAve,
}
}
func (m *muxerMetrics) update(connectionID string, metrics *h2mux.MuxerMetrics) {
m.rtt.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTT))
m.rttMin.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTTMin))
m.rttMax.WithLabelValues(connectionID).Set(convertRTTMilliSec(metrics.RTTMax))
m.receiveWindowAve.WithLabelValues(connectionID).Set(metrics.ReceiveWindowAve)
m.sendWindowAve.WithLabelValues(connectionID).Set(metrics.SendWindowAve)
m.receiveWindowMin.WithLabelValues(connectionID).Set(float64(metrics.ReceiveWindowMin))
m.receiveWindowMax.WithLabelValues(connectionID).Set(float64(metrics.ReceiveWindowMax))
m.sendWindowMin.WithLabelValues(connectionID).Set(float64(metrics.SendWindowMin))
m.sendWindowMax.WithLabelValues(connectionID).Set(float64(metrics.SendWindowMax))
m.inBoundRateCurr.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateCurr))
m.inBoundRateMin.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateMin))
m.inBoundRateMax.WithLabelValues(connectionID).Set(float64(metrics.InBoundRateMax))
m.outBoundRateCurr.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateCurr))
m.outBoundRateMin.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMin))
m.outBoundRateMax.WithLabelValues(connectionID).Set(float64(metrics.OutBoundRateMax))
m.compBytesBefore.WithLabelValues(connectionID).Set(float64(metrics.CompBytesBefore.Value()))
m.compBytesAfter.WithLabelValues(connectionID).Set(float64(metrics.CompBytesAfter.Value()))
m.compRateAve.WithLabelValues(connectionID).Set(float64(metrics.CompRateAve()))
}
func convertRTTMilliSec(t time.Duration) float64 {
return float64(t / time.Millisecond)
}
// Metrics that can be collected without asking the edge // Metrics that can be collected without asking the edge
func initTunnelMetrics() *tunnelMetrics { func initTunnelMetrics() *tunnelMetrics {
maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec( maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec(
@ -351,15 +79,6 @@ func initTunnelMetrics() *tunnelMetrics {
) )
prometheus.MustRegister(maxConcurrentRequestsPerTunnel) prometheus.MustRegister(maxConcurrentRequestsPerTunnel)
timerRetries := prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: TunnelSubsystem,
Name: "timer_retries",
Help: "Unacknowledged heart beats count",
})
prometheus.MustRegister(timerRetries)
serverLocations := prometheus.NewGaugeVec( serverLocations := prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: MetricsNamespace, Namespace: MetricsNamespace,
@ -416,10 +135,8 @@ func initTunnelMetrics() *tunnelMetrics {
prometheus.MustRegister(registerSuccess) prometheus.MustRegister(registerSuccess)
return &tunnelMetrics{ return &tunnelMetrics{
timerRetries: timerRetries,
serverLocations: serverLocations, serverLocations: serverLocations,
oldServerLocations: make(map[string]string), oldServerLocations: make(map[string]string),
muxerMetrics: newMuxerMetrics(),
tunnelsHA: newTunnelsForHA(), tunnelsHA: newTunnelsForHA(),
regSuccess: registerSuccess, regSuccess: registerSuccess,
regFail: registerFail, regFail: registerFail,
@ -429,10 +146,6 @@ func initTunnelMetrics() *tunnelMetrics {
} }
} }
func (t *tunnelMetrics) updateMuxerMetrics(connectionID string, metrics *h2mux.MuxerMetrics) {
t.muxerMetrics.update(connectionID, metrics)
}
func (t *tunnelMetrics) registerServerLocation(connectionID, loc string) { func (t *tunnelMetrics) registerServerLocation(connectionID, loc string) {
t.locationLock.Lock() t.locationLock.Lock()
defer t.locationLock.Unlock() defer t.locationLock.Unlock()

View File

@ -47,7 +47,6 @@ func (o *Observer) RegisterSink(sink EventSink) {
} }
func (o *Observer) logConnected(connectionID uuid.UUID, connIndex uint8, location string, address net.IP, protocol Protocol) { func (o *Observer) logConnected(connectionID uuid.UUID, connIndex uint8, location string, address net.IP, protocol Protocol) {
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
o.log.Info(). o.log.Info().
Int(management.EventTypeKey, int(management.Cloudflared)). Int(management.EventTypeKey, int(management.Cloudflared)).
Str(LogFieldConnectionID, connectionID.String()). Str(LogFieldConnectionID, connectionID.String()).
@ -63,8 +62,8 @@ func (o *Observer) sendRegisteringEvent(connIndex uint8) {
o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel}) o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel})
} }
func (o *Observer) sendConnectedEvent(connIndex uint8, protocol Protocol, location string) { func (o *Observer) sendConnectedEvent(connIndex uint8, protocol Protocol, location string, edgeAddress net.IP) {
o.sendEvent(Event{Index: connIndex, EventType: Connected, Protocol: protocol, Location: location}) o.sendEvent(Event{Index: connIndex, EventType: Connected, Protocol: protocol, Location: location, EdgeAddress: edgeAddress})
} }
func (o *Observer) SendURL(url string) { func (o *Observer) SendURL(url string) {

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