Compare commits

..

287 Commits

Author SHA1 Message Date
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
Devin Carr 9584adc38a Release 2023.7.2 2023-07-21 15:31:10 -07:00
Devin Carr 0096f2613c TUN-7587: Remove junos builds 2023-07-20 18:29:33 +00:00
João Oliveirinha ac82c8b08b TUN-7599: Onboard cloudflared to Software Dashboard 2023-07-19 13:30:35 +00:00
João "Pisco" Fernandes af3a66d60e TUN-7597: Add flag to disable auto-update services to be installed
Summary:
This commit adds a new flag "no-update-service" to the `cloudflared service install` command.

Previously, when installing cloudflared as a linux service it would always get auto-updates, now with this new flag it is possible to disable the auto updates of the service.

This flag allows to define whether we want cloudflared service to **perform auto updates or not**.
For **systemd this is done by removing the installation of the update service and timer**, for **sysv** this is done by **setting the cloudflared autoupdate flag**.
2023-07-19 11:06:11 +00:00
Devin Carr 42e0540395 TUN-7588: Update package coreos/go-systemd 2023-07-18 18:57:32 +00:00
Devin Carr 2ee90483bf TUN-7585: Remove h2mux compression
h2mux is already deprecated and will be eventually removed, in the meantime,
the compression tests cause flaky failures. Removing them and the brotli
code slims down our binaries and dependencies on CGO.
2023-07-18 18:14:19 +00:00
Devin Carr 2084a123c2 TUN-7594: Add nightly arm64 cloudflared internal deb publishes 2023-07-17 15:04:17 -07:00
Devin Carr b500e556bf TUN-7590: Remove usages of ioutil 2023-07-17 19:08:38 +00:00
Devin Carr 1b0b6bf7a8 TUN-7589: Remove legacy golang.org/x/crypto/ssh/terminal package usage
Package has been moved to golang.org/x/term
2023-07-17 19:02:15 +00:00
Devin Carr 85eee4849f TUN-7586: Upgrade go-jose/go-jose/v3 and core-os/go-oidc/v3
Removes usages of gopkg.in/square/go-jose.v2 and gopkg.in/coreos/go-oidc.v2 packages.
2023-07-17 19:02:03 +00:00
Devin Carr 9b8a533435 Release 2023.7.1 2023-07-13 12:31:33 -07:00
Devin Carr 5abb90b539 TUN-7582: Correct changelog wording for --management-diagnostics 2023-07-13 09:47:21 -07:00
João Oliveirinha 0c8bc56930 TUN-7575: Add option to disable PTMU discovery over QUIC
This commit implements the option to disable PTMU discovery for QUIC
connections.
QUIC finds the PMTU during startup by increasing Ping packet frames
until Ping responses are not received anymore, and it seems to stick
with that PMTU forever.

This is no problem if the PTMU doesn't change over time, but if it does
it may case packet drops.
We add this hidden flag for debugging purposes in such situations as a
quick way to validate if problems that are being seen can be solved by
reducing the packet size to the edge.

Note however, that this option may impact UDP proxying since we expect
being able to send UDP packets of 1280 bytes over QUIC.
So, this option should not be used when tunnel is being used for UDP
proxying.
2023-07-13 10:24:24 +01:00
Devin Carr fdab68aa08 Release 2023.7.0 2023-07-11 10:28:45 -07:00
Devin Carr 5aaab967a3 TUN-7477: Decrement UDP sessions on shutdown
When a tunnel connection is going down, any active UDP sessions
need to be cleared and the metric needs to be decremented.
2023-07-06 22:14:53 +00:00
Devin Carr ccad59dfab TUN-7564: Support cf-trace-id for cloudflared access 2023-07-06 19:03:40 +00:00
Devin Carr 8a3eade6d3 TUN-7553: Add flag to enable management diagnostic services
With the new flag --management-diagnostics (an opt-in flag)
cloudflared's will be able to report additional diagnostic information
over the management.argotunnel.com request path.
Additions include the /metrics prometheus endpoint; which is already
bound to a local port via --metrics.
/debug/pprof/(goroutine|heap) are also provided to allow for remotely
retrieving heap information from a running cloudflared connector.
2023-07-06 17:31:11 +00:00
Sudarsan Reddy 39847a70f2 TUN-7558: Flush on Writes for StreamBasedOriginProxy
In the streambased origin proxy flow (example ssh over access), there is
a chance when we do not flush on http.ResponseWriter writes. This PR
guarantees that the response writer passed to proxy stream has a flusher
embedded after writes. This means we write much more often back to the
ResponseWriter and are not waiting. Note, this is only something we do
when proxyHTTP-ing to a StreamBasedOriginProxy because we do not want to
have situations where we are not sending information that is needed by
the other side (eyeball).
2023-07-06 14:22:29 +00:00
João Oliveirinha d1e338ee48 TUN-7545: Add support for full bidirectionally streaming with close signal propagation 2023-07-06 11:54:26 +01:00
Devin Carr b243602d1c TUN-7550: Add pprof endpoint to management service 2023-07-05 20:29:00 +00:00
Devin Carr 960c5a7baf TUN-7551: Complete removal of raven-go to sentry-go
Removes the final usage of raven-go and removes the dependency.
2023-06-30 14:11:55 -07:00
Devin Carr aca3575b6d TUN-7549: Add metrics route to management service 2023-06-30 09:38:26 -07:00
Devin Carr 2b4815a9f5 TUN-7543: Add --debug-stream flag to cloudflared access ssh
Allows for debugging the payloads that are sent in client mode to
the ssh server. Required to be run with --log-directory to capture
logging output. Additionally has maximum limit that is provided with
the flag that will only capture the first N number of reads plus
writes through the WebSocket stream. These reads/writes are not directly
captured at the packet boundary so some reconstruction from the
log messages will be required.

Added User-Agent for all out-going cloudflared access
tcp requests in client mode.
Added check to not run terminal logging in cloudflared access tcp
client mode to not obstruct the stdin and stdout.
2023-06-29 10:29:15 -07:00
João "Pisco" Fernandes 729890d847 TUN-6011: Remove docker networks from ICMP Proxy test 2023-06-27 17:33:18 +01:00
EduardoGomes 31f424d589 AUTH-5328 Pass cloudflared_token_check param when running cloudflared access login 2023-06-20 11:48:38 +01:00
Sudarsan Reddy cb4bd8d065 Release 2023.6.1 2023-06-20 09:24:26 +01:00
Sudarsan Reddy 1abd22ef0a TUN-7480: Added a timeout for unregisterUDP.
I deliberately kept this as an unregistertimeout because that was the
intent. In the future we could change this to a UDPConnConfig if we want
to pass multiple values here.

The idea of this PR is simply to add a configurable unregister UDP
timeout.
2023-06-20 06:20:09 +00:00
Devin Carr a3bcf25fae TUN-7477: Add UDP/TCP session metrics
New gauge metrics are exposed in the prometheus endpoint to
capture the current and total TCP and UDP sessions that
cloudflared has proxied.
2023-06-19 16:28:37 +00:00
João Oliveirinha 20e36c5bf3 TUN-7468: Increase the limit of incoming streams 2023-06-19 10:41:56 +00:00
João "Pisco" Fernandes 5693ba524b Release 2023.6.0 2023-06-15 15:06:11 +01:00
João Oliveirinha 9c6fbfca18 TUN-7471: Fixes cloudflared not closing the quic stream on unregister UDP session
This code was leaking streams because it wasn't closing the quic stream
after unregistering from the edge.
2023-06-15 10:52:32 +01:00
João "Pisco" Fernandes 925ec100d6 TUN-7463: Add default ingress rule if no ingress rules are provided when updating the configuration 2023-06-12 15:11:42 +01:00
Sudarsan Reddy 58b27a1ccf TUN-7447: Add a cover build to report code coverage 2023-05-31 14:59:05 +01:00
Devin Carr 867360c8dd Release 2023.5.1 2023-05-23 10:07:25 -07:00
Devin Carr cb97257815 TUN-7424: Add CORS headers to host_details responses 2023-05-16 22:18:57 -07:00
Devin Carr c43e07d6b7 TUN-7421: Add *.cloudflare.com to permitted Origins for management WebSocket requests 2023-05-11 10:13:39 -07:00
Devin Carr 9426b60308 TUN-7227: Migrate to devincarr/quic-go
The lucas-clemente/quic-go package moved namespaces and our branch
went stale, this new fork provides support for the new quic-go repo
and applies the max datagram frame size change.

Until the max datagram frame size support gets upstreamed into quic-go,
this can be used to unblock go 1.20 support as the old
lucas-clemente/quic-go will not get go 1.20 support.
2023-05-10 19:44:15 +00:00
Devin Carr ff9621bbd5 TUN-7404: Default configuration version set to -1
We need to set the default configuration to -1 to accommodate local
to remote configuration migrations that will set the configuration
version to 0. This make's sure to override the local configuration
with the new remote configuration when sent as it does a check against
the local current configuration version.
2023-05-05 12:47:17 -07:00
Devin Carr 7a0a618c0d Release 2023.5.0 2023-05-01 11:29:26 -07:00
João Oliveirinha 0be1ed5284 TUN-7398: Add support for quic safe stream to set deadline 2023-04-27 19:49:56 +01:00
Devin Carr 50a0c44cee TUN-7392: Ignore release checksum upload if asset already uploaded 2023-04-26 13:46:35 -07:00
Devin Carr 76391434c2 TUN-7393: Add json output for cloudflared tail
cloudflared tail now has a `--output=json` that will allow it to easily pipe into tools like jq for a more structured view of the streaming logs.
2023-04-26 15:41:00 +00:00
Sudarsan Reddy e8841c0fb3 TUN-7394: Retry StartFirstTunnel on quic.ApplicationErrors
This PR adds ApplicationError as one of the "try_again" error types for
startfirstTunnel. This ensures that these kind of errors (which we've
seen occur when a tunnel gets rate-limited) are retried.
2023-04-26 12:58:01 +01:00
Devin Carr aec1d8f653 TUN-7392: Ignore duplicate artifact uploads for github release 2023-04-25 21:44:24 +00:00
Devin Carr c7f343a3b4 TUN-7390: Remove Debian stretch builds 2023-04-25 21:44:08 +00:00
2488 changed files with 156844 additions and 199981 deletions
+3 -3
View File
@@ -4,15 +4,15 @@ jobs:
check:
strategy:
matrix:
go-version: [1.19.x]
go-version: [1.22.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Test
run: make test
+24
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
View File
@@ -17,3 +17,4 @@ cscope.*
ssh_server_tests/.env
/.cover
built_artifacts/
component-tests/.venv
+8
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/f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38 is version go1.22.5-devel-cf
git checkout -q f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38
./make.bash
+29 -18
View File
@@ -7,13 +7,17 @@ if [[ "$(uname)" != "Darwin" ]] ; then
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
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")
@@ -25,10 +29,11 @@ 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
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
@@ -138,22 +143,28 @@ else
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
# 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
# TODO: TUN-5789
fi
ARCH_TARGET_DIRECTORY="${TARGET_DIRECTORY}/${TARGET_ARCH}-build"
# creating build directory
rm -rf $TARGET_DIRECTORY
mkdir "${TARGET_DIRECTORY}"
mkdir "${TARGET_DIRECTORY}/contents"
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
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} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
cp ${BINARY_NAME} "${ARCH_TARGET_DIRECTORY}/contents/${PRODUCT}"
# compress cloudflared into a tar and gzipped file
tar czf "$FILENAME" "${BINARY_NAME}"
@@ -162,23 +173,23 @@ tar czf "$FILENAME" "${BINARY_NAME}"
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--scripts ${ARCH_TARGET_DIRECTORY}/scripts \
--root ${ARCH_TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
--sign "${PKG_SIGN_NAME}" \
${PKGNAME}
# notarize the package
# TODO: https://jira.cfdata.org/browse/TUN-5789
# TODO: TUN-5789
else
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--scripts ${ARCH_TARGET_DIRECTORY}/scripts \
--root ${ARCH_TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PKGNAME}
fi
# cleaning up the build directory
rm -rf $TARGET_DIRECTORY
# cleanup build directory because this script is not ran within containers,
# which might lead to future issues in subsequent runs.
rm -rf "${TARGET_DIRECTORY}"
+10
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
+8 -6
View File
@@ -3,15 +3,17 @@ echo $VERSION
export TARGET_OS=windows
# This controls the directory the built artifacts go into
export ARTIFACT_DIR=built_artifacts/
mkdir -p $ARTIFACT_DIR
export BUILT_ARTIFACT_DIR=built_artifacts/
export FINAL_ARTIFACT_DIR=artifacts/
mkdir -p $BUILT_ARTIFACT_DIR
mkdir -p $FINAL_ARTIFACT_DIR
windowsArchs=("amd64" "386")
for arch in ${windowsArchs[@]}; do
export TARGET_ARCH=$arch
# Copy exe into final directory
cp ./artifacts/cloudflared-windows-$arch.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe
cp ./artifacts/cloudflared-windows-$arch.exe ./cloudflared.exe
cp $BUILT_ARTIFACT_DIR/cloudflared-windows-$arch.exe ./cloudflared.exe
make cloudflared-msi
# Copy msi into final directory
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
done
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
-26
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
-66
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
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
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
+16
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/f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38 is version go1.22.5-devel-cf
git checkout -q f4334cdc0c3f22a3bfdd7e66f387e3ffc65a5c38
& ./make.bat
Write-Output "Installed"
+20
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"
+23
View File
@@ -1,3 +1,26 @@
## 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
### 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.
## 2023.4.1
### New Features
- You can now stream your logs from your remote cloudflared to your local terminal with `cloudflared tail <TUNNEL-ID>`. This new feature requires the remote cloudflared to be version 2023.4.1 or higher.
+8 -4
View File
@@ -1,19 +1,23 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.19 as builder
FROM golang:1.22.5 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
TARGET_GOARCH=${TARGET_GOARCH}
TARGET_GOARCH=${TARGET_GOARCH} \
CONTAINER_BUILD=1
WORKDIR /go/src/github.com/cloudflare/cloudflared/
# copy our sources into the builder image
COPY . .
RUN .teamcity/install-cloudflare-go.sh
# compile cloudflared
RUN make cloudflared
RUN PATH="/tmp/go/bin:$PATH" make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian11:nonroot
+6 -4
View File
@@ -1,15 +1,17 @@
# use a builder image for building cloudflare
FROM golang:1.19 as builder
FROM golang:1.22.5 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0
CGO_ENABLED=0
WORKDIR /go/src/github.com/cloudflare/cloudflared/
# copy our sources into the builder image
COPY . .
RUN .teamcity/install-cloudflare-go.sh
# 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
FROM gcr.io/distroless/base-debian11:nonroot
+6 -4
View File
@@ -1,15 +1,17 @@
# use a builder image for building cloudflare
FROM golang:1.19 as builder
FROM golang:1.22.5 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0
CGO_ENABLED=0
WORKDIR /go/src/github.com/cloudflare/cloudflared/
# copy our sources into the builder image
COPY . .
RUN .teamcity/install-cloudflare-go.sh
# 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
FROM gcr.io/distroless/base-debian11:nonroot-arm64
+54 -105
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].*.*")
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.
@@ -27,6 +30,10 @@ ifdef PACKAGE_MANAGER
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)"
endif
ifdef CONTAINER_BUILD
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/metrics.Runtime=virtual"
endif
LINK_FLAGS :=
ifeq ($(FIPS), true)
LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS)
@@ -49,6 +56,8 @@ PACKAGE_DIR := $(CURDIR)/packaging
PREFIX := /usr
INSTALL_BINDIR := $(PREFIX)/bin/
INSTALL_MANDIR := $(PREFIX)/share/man/man1/
CF_GO_PATH := /tmp/go
PATH := $(CF_GO_PATH)/bin:$(PATH)
LOCAL_ARCH ?= $(shell uname -m)
ifneq ($(GOARCH),)
@@ -126,7 +135,7 @@ ifeq ($(FIPS), true)
$(info Building cloudflared with go-fips)
cp -f fips/fips.go.linux-amd64 cmd/cloudflared/fips.go
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)
rm -f cmd/cloudflared/fips.go
./check-fips.sh cloudflared
@@ -140,6 +149,7 @@ container:
generate-docker-version:
echo latest $(VERSION) > versions
.PHONY: test
test: vet
ifndef CI
@@ -147,17 +157,43 @@ ifndef CI
else
@mkdir -p .cover
go test -v -mod=vendor -race $(LDFLAGS) -coverprofile=".cover/c.out" ./...
go tool cover -html ".cover/c.out" -o .cover/all.html
endif
.PHONY: test-ssh-server
test-ssh-server:
docker-compose -f ssh_server_tests/docker-compose.yml up
.PHONY: cover
cover:
@echo ""
@echo "=====> Total test coverage: <====="
@echo ""
# Print the overall coverage here for quick access.
$Q go tool cover -func ".cover/c.out" | grep "total:" | awk '{print $$3}'
# 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
.PHONY: fuzz
fuzz:
@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
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)
install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared
install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1
@@ -194,116 +230,29 @@ cloudflared-pkg: cloudflared cloudflared.1
cloudflared-msi:
wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs
.PHONY: cloudflared-darwin-amd64.tgz
cloudflared-darwin-amd64.tgz: cloudflared
tar czf cloudflared-darwin-amd64.tgz cloudflared
rm cloudflared
.PHONY: cloudflared-junos
cloudflared-junos: cloudflared jetez-certificate.pem jetez-key.pem
jetez --source . \
-j jet.yaml \
--key jetez-key.pem \
--cert jetez-certificate.pem \
--version $(VERSION)
rm jetez-*.pem
jetez-certificate.pem:
ifndef JETEZ_CERT
$(error JETEZ_CERT not defined)
endif
@echo "Writing JetEZ certificate"
@echo "$$JETEZ_CERT" > jetez-certificate.pem
jetez-key.pem:
ifndef JETEZ_KEY
$(error JETEZ_KEY not defined)
endif
@echo "Writing JetEZ key"
@echo "$$JETEZ_KEY" > jetez-key.pem
.PHONY: publish-cloudflared-junos
publish-cloudflared-junos: cloudflared-junos cloudflared-x86-64.latest.s3
ifndef S3_ENDPOINT
$(error S3_HOST not defined)
endif
ifndef S3_URI
$(error S3_URI not defined)
endif
ifndef S3_ACCESS_KEY
$(error S3_ACCESS_KEY not defined)
endif
ifndef S3_SECRET_KEY
$(error S3_SECRET_KEY not defined)
endif
sha256sum cloudflared-x86-64-$(VERSION).tgz | awk '{printf $$1}' > cloudflared-x86-64-$(VERSION).tgz.shasum
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
put cloudflared-x86-64-$(VERSION).tgz $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
put cloudflared-x86-64-$(VERSION).tgz.shasum $(S3_URI)/cloudflared-x86-64-$(VERSION).tgz.shasum
dpkg --compare-versions "$(VERSION)" gt "$(shell cat cloudflared-x86-64.latest.s3)" && \
echo -n "$(VERSION)" > cloudflared-x86-64.latest && \
s4cmd --endpoint-url $(S3_ENDPOINT) --force --API-GrantRead=uri=http://acs.amazonaws.com/groups/global/AllUsers \
put cloudflared-x86-64.latest $(S3_URI)/cloudflared-x86-64.latest || \
echo "Latest version not updated"
cloudflared-x86-64.latest.s3:
s4cmd --endpoint-url $(S3_ENDPOINT) --force \
get $(S3_URI)/cloudflared-x86-64.latest cloudflared-x86-64.latest.s3
.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-dryrun
github-release-dryrun:
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION) --dry-run
.PHONY: github-release
github-release: cloudflared
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
.PHONY: github-release-built-pkgs
github-release-built-pkgs:
github-release:
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)
.PHONY: github-mac-upload
github-mac-upload:
python3 github_release.py --path artifacts/cloudflared-darwin-amd64.tgz --release-version $(VERSION) --name cloudflared-darwin-amd64.tgz
python3 github_release.py --path artifacts/cloudflared-amd64.pkg --release-version $(VERSION) --name cloudflared-amd64.pkg
.PHONY: r2-linux-release
r2-linux-release:
python3 ./release_pkgs.py
.PHONY: github-windows-upload
github-windows-upload:
python3 github_release.py --path built_artifacts/cloudflared-windows-amd64.exe --release-version $(VERSION) --name cloudflared-windows-amd64.exe
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
tunnelrpc-deps:
.PHONY: capnp
capnp:
which capnp # https://capnproto.org/install.html
which capnpc-go # go install zombiezen.com/go/capnproto2/capnpc-go@latest
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
.PHONY: quic-deps
quic-deps:
which capnp
which capnpc-go
capnp compile -ogo quic/schema/quic_metadata_protocol.capnp
capnp compile -ogo tunnelrpc/proto/tunnelrpc.capnp tunnelrpc/proto/quic_metadata_protocol.capnp
.PHONY: vet
vet:
go vet -v -mod=vendor github.com/cloudflare/cloudflared/...
go vet -mod=vendor github.com/cloudflare/cloudflared/...
.PHONY: 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)
+4 -7
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)
* 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)
* 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
@@ -49,13 +49,10 @@ Once installed, you can authenticate `cloudflared` into your Cloudflare account
## 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
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 |
|---|---|
| 2020.5.1 and later | Supported |
| Versions prior to 2020.5.1 | No longer supported |
For example, as of January 2023 Cloudflare will support cloudflared version 2023.1.1 to cloudflared 2022.1.1.
+298
View File
@@ -1,3 +1,301 @@
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-07-19 TUN-7599: Onboard cloudflared to Software Dashboard
- 2023-07-19 TUN-7587: Remove junos builds
- 2023-07-18 TUN-7597: Add flag to disable auto-update services to be installed
- 2023-07-17 TUN-7594: Add nightly arm64 cloudflared internal deb publishes
- 2023-07-14 TUN-7586: Upgrade go-jose/go-jose/v3 and core-os/go-oidc/v3
- 2023-07-14 TUN-7589: Remove legacy golang.org/x/crypto/ssh/terminal package usage
- 2023-07-14 TUN-7590: Remove usages of ioutil
- 2023-07-14 TUN-7585: Remove h2mux compression
- 2023-07-14 TUN-7588: Update package coreos/go-systemd
2023.7.1
- 2023-07-13 TUN-7582: Correct changelog wording for --management-diagnostics
- 2023-07-12 TUN-7575: Add option to disable PTMU discovery over QUIC
2023.7.0
- 2023-07-06 TUN-7558: Flush on Writes for StreamBasedOriginProxy
- 2023-07-05 TUN-7553: Add flag to enable management diagnostic services
- 2023-07-05 TUN-7564: Support cf-trace-id for cloudflared access
- 2023-07-05 TUN-7477: Decrement UDP sessions on shutdown
- 2023-07-03 TUN-7545: Add support for full bidirectionally streaming with close signal propagation
- 2023-06-30 TUN-7549: Add metrics route to management service
- 2023-06-30 TUN-7551: Complete removal of raven-go to sentry-go
- 2023-06-30 TUN-7550: Add pprof endpoint to management service
- 2023-06-29 TUN-7543: Add --debug-stream flag to cloudflared access ssh
- 2023-06-26 TUN-6011: Remove docker networks from ICMP Proxy test
- 2023-06-20 AUTH-5328 Pass cloudflared_token_check param when running cloudflared access login
2023.6.1
- 2023-06-19 TUN-7480: Added a timeout for unregisterUDP.
- 2023-06-16 TUN-7477: Add UDP/TCP session metrics
- 2023-06-14 TUN-7468: Increase the limit of incoming streams
2023.6.0
- 2023-06-15 TUN-7471: Fixes cloudflared not closing the quic stream on unregister UDP session
- 2023-06-09 TUN-7463: Add default ingress rule if no ingress rules are provided when updating the configuration
- 2023-05-31 TUN-7447: Add a cover build to report code coverage
2023.5.1
- 2023-05-16 TUN-7424: Add CORS headers to host_details responses
- 2023-05-11 TUN-7421: Add *.cloudflare.com to permitted Origins for management WebSocket requests
- 2023-05-05 TUN-7404: Default configuration version set to -1
- 2023-05-05 TUN-7227: Migrate to devincarr/quic-go
2023.5.0
- 2023-04-27 TUN-7398: Add support for quic safe stream to set deadline
- 2023-04-26 TUN-7394: Retry StartFirstTunnel on quic.ApplicationErrors
- 2023-04-26 TUN-7392: Ignore release checksum upload if asset already uploaded
- 2023-04-25 TUN-7392: Ignore duplicate artifact uploads for github release
- 2023-04-25 TUN-7393: Add json output for cloudflared tail
- 2023-04-24 TUN-7390: Remove Debian stretch builds
2023.4.2
- 2023-04-24 TUN-7133: Add sampling support for streaming logs
- 2023-04-21 TUN-7141: Add component tests for streaming logs
+3 -2
View File
@@ -1,8 +1,9 @@
#!/bin/bash
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
echo $VERSION
# This controls the directory the built artifacts go into
export ARTIFACT_DIR=built_artifacts/
export ARTIFACT_DIR=artifacts/
mkdir -p $ARTIFACT_DIR
arch=("amd64")
@@ -22,4 +23,4 @@ make cloudflared-rpm
mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm
# finally move the linux binary as well.
mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch
mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch
+10 -2
View File
@@ -1,11 +1,13 @@
#!/bin/bash
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
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
# This controls the directory the built artifacts go into
export ARTIFACT_DIR=built_artifacts/
export ARTIFACT_DIR=artifacts/
mkdir -p $ARTIFACT_DIR
linuxArchs=("386" "amd64" "arm" "armhf" "arm64")
@@ -13,6 +15,12 @@ export TARGET_OS=linux
for arch in ${linuxArchs[@]}; do
unset TARGET_ARM
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
if [[ $arch == armhf ]] ; then
+16
View File
@@ -0,0 +1,16 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: cloudflared
description: Client for Cloudflare Tunnels
annotations:
backstage.io/source-location: url:https://bitbucket.cfdata.org/projects/TUN/repos/cloudflared/browse
cloudflare.com/software-excellence-opt-in: "true"
cloudflare.com/jira-project-key: "TUN"
cloudflare.com/jira-project-component: "Cloudflare Tunnel"
tags:
- internal
spec:
type: "service"
lifecycle: "Active"
owner: "teams/tunnel-teams-routing"
+69 -8
View File
@@ -109,20 +109,34 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (
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.
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
var result response
// First, parse the wrapper and check the API call succeeded
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 {
return err
return nil, err
}
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
// result into the datatype provided as a parameter.
if err := json.Unmarshal(result.Result, &data); err != nil {
@@ -131,11 +145,58 @@ func parseResponse(reader io.Reader, data interface{}) error {
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 {
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,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 {
+2 -2
View File
@@ -9,7 +9,7 @@ type TunnelClient interface {
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
GetTunnelToken(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)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
@@ -22,7 +22,7 @@ type HostnameClient interface {
type IPRouteClient interface {
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
AddRoute(newRoute NewRoute) (Route, error)
DeleteRoute(params DeleteRouteParams) error
DeleteRoute(id uuid.UUID) error
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
}
+23 -28
View File
@@ -75,10 +75,12 @@ type NewRoute struct {
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
func (r NewRoute) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Network string `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"`
Comment string `json:"comment"`
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
}{
Network: r.Network.String(),
TunnelID: r.TunnelID,
Comment: r.Comment,
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.
type DetailedRoute struct {
ID uuid.UUID `json:"id"`
Network CIDR `json:"network"`
TunnelID uuid.UUID `json:"tunnel_id"`
// 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(
"%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(),
vnetColumn,
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 {
Ip net.IP
// 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.
// Due to pagination on the server side it will call the endpoint multiple times if needed.
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
endpoint := r.baseEndpoints.accountRoutes
endpoint.RawQuery = filter.Encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
fetchFn := func(page int) (*http.Response, error) {
endpoint := r.baseEndpoints.accountRoutes
filter.Page(page)
endpoint.RawQuery = filter.Encode()
rsp, err := r.sendRequest("GET", endpoint, nil)
if resp.StatusCode == http.StatusOK {
return parseListDetailedRoutes(resp.Body)
if err != nil {
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 nil, r.statusCodeToError("list routes", resp)
return fetchExhaustively[DetailedRoute](fetchFn)
}
// AddRoute calls the Tunnelstore POST endpoint for a given route.
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
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)
if err != nil {
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.
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
endpoint := r.baseEndpoints.accountRoutes
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
setVnetParam(&endpoint, params.VNetID)
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
resp, err := r.sendRequest("DELETE", endpoint, 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)
}
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
var routes []*DetailedRoute
err := parseResponse(body, &routes)
return routes, err
}
func parseRoute(body io.ReadCloser) (Route, error) {
var route Route
err := parseResponse(body, &route)
+29 -18
View File
@@ -58,31 +58,29 @@ type IpRouteFilter struct {
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
f := &IpRouteFilter{
queryParams: url.Values{},
}
f := NewIPRouteFilter()
// Set deletion filter
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
f.deleted()
f.Deleted()
} else {
f.notDeleted()
f.NotDeleted()
}
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
return nil, err
} else if subset != nil {
f.networkIsSupersetOf(*subset)
f.NetworkIsSupersetOf(*subset)
}
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
return nil, err
} else if superset != nil {
f.networkIsSupersetOf(*superset)
f.NetworkIsSupersetOf(*superset)
}
if comment := c.String(filterIpRouteComment.Name); comment != "" {
f.commentIs(comment)
f.CommentIs(comment)
}
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
@@ -90,7 +88,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
if err != nil {
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 != "" {
@@ -98,7 +96,7 @@ func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
if err != nil {
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 {
@@ -124,35 +122,44 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
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)
}
func (f *IpRouteFilter) notDeleted() {
func (f *IpRouteFilter) NotDeleted() {
f.queryParams.Set("is_deleted", "false")
}
func (f *IpRouteFilter) deleted() {
func (f *IpRouteFilter) Deleted() {
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())
}
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
func (f *IpRouteFilter) NetworkIsSupersetOf(subset net.IPNet) {
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))
}
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
func (f *IpRouteFilter) TunnelID(id uuid.UUID) {
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())
}
@@ -160,6 +167,10 @@ func (f *IpRouteFilter) MaxFetchSize(max uint) {
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 {
return f.queryParams.Encode()
}
+4 -1
View File
@@ -69,6 +69,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
}{
{
`{
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"comment":"test",
@@ -80,6 +81,7 @@ func TestDetailedRouteJsonRoundtrip(t *testing.T) {
},
{
`{
"id":"91ebc578-cc99-4641-9937-0fb630505fa0",
"network":"10.1.2.40/29",
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
@@ -167,9 +169,10 @@ func TestRouteTableString(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, network)
r := DetailedRoute{
ID: uuid.Nil,
Network: CIDR(*network),
}
row := r.TableString()
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"))
}
+21 -19
View File
@@ -93,7 +93,7 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWith
switch resp.StatusCode {
case http.StatusOK:
var tunnel TunnelWithToken
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil {
if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
return nil, serdeErr
}
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)
}
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
endpoint := r.baseEndpoints.accountLevel
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)
if err != nil {
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) {
endpoint := r.baseEndpoints.accountLevel
endpoint.RawQuery = filter.encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return parseListTunnels(resp.Body)
fetchFn := func(page int) (*http.Response, error) {
endpoint := r.baseEndpoints.accountLevel
filter.Page(page)
endpoint.RawQuery = filter.encode()
rsp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
if rsp.StatusCode != http.StatusOK {
rsp.Body.Close()
return nil, r.statusCodeToError("list tunnels", rsp)
}
return rsp, nil
}
return nil, r.statusCodeToError("list tunnels", resp)
}
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
var tunnels []*Tunnel
err := parseResponse(body, &tunnels)
return tunnels, err
return fetchExhaustively[Tunnel](fetchFn)
}
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
+4
View File
@@ -50,6 +50,10 @@ func (f *TunnelFilter) MaxFetchSize(max uint) {
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 {
return f.queryParams.Encode()
}
-47
View File
@@ -3,7 +3,6 @@ package cfapi
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"reflect"
"strings"
@@ -16,52 +15,6 @@ import (
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 := ioutil.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) {
type args struct {
body string
+115 -133
View File
@@ -1,92 +1,69 @@
pinned_go: &pinned_go go=1.19.6-1
pinned_go_fips: &pinned_go_fips go-boring=1.19.6-1
pinned_go: &pinned_go go-boring=1.22.5-1
build_dir: &build_dir /cfsetup_build
default-flavor: bullseye
stretch: &stretch
build:
default-flavor: bookworm
bullseye: &bullseye
build-linux:
build_dir: *build_dir
builddeps: &build_deps
- *pinned_go
- build-essential
- gotest-to-teamcity
- fakeroot
- rubygem-fpm
- rpm
- libffi-dev
pre-cache: &build_pre_cache
- export GOCACHE=/cfsetup_build/.cache/go-build
- go install golang.org/x/tools/cmd/goimports@latest
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- make cloudflared
build-fips:
# Build binary for component test
- GOOS=linux GOARCH=amd64 make cloudflared
build-linux-fips:
build_dir: *build_dir
builddeps: &build_deps_fips
- *pinned_go_fips
- build-essential
- gotest-to-teamcity
builddeps: *build_deps
pre-cache: *build_pre_cache
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- export FIPS=true
- make cloudflared
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
github-release-pkgs:
# Build binary for component test
- GOOS=linux GOARCH=amd64 make cloudflared
cover:
build_dir: *build_dir
builddeps:
builddeps: *build_deps
pre-cache: *build_pre_cache
post-cache:
- make cover
# except FIPS and macos
build-linux-release:
build_dir: *build_dir
builddeps: &build_deps_release
- *pinned_go
- 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-dev
- python3-pip
- reprepro
- createrepo
pre-cache: &github_release_pkgs_pre_cache
- 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
- python3-setuptools
- wget
- python3-venv
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-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
github-fips-release-pkgs:
build-linux-fips-release:
build_dir: *build_dir
builddeps:
- *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
builddeps: *build_deps_release
post-cache:
# same logic as above, but for FIPS packages only
- ./build-packages-fips.sh
- make github-release-built-pkgs
generate-versions-file:
build_dir: *build_dir
builddeps:
builddeps:
- *pinned_go
- build-essential
post-cache:
@@ -105,7 +82,7 @@ stretch: &stretch
build-fips-internal-deb:
build_dir: *build_dir
builddeps: &build_fips_deb_deps
- *pinned_go_fips
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
@@ -115,7 +92,7 @@ stretch: &stretch
- export FIPS=true
- export ORIGINAL_NAME=true
- make cloudflared-deb
build-fips-internal-deb-nightly:
build-internal-deb-nightly-amd64:
build_dir: *build_dir
builddeps: *build_fips_deb_deps
post-cache:
@@ -125,6 +102,16 @@ stretch: &stretch
- export FIPS=true
- export ORIGINAL_NAME=true
- make cloudflared-deb
build-internal-deb-nightly-arm64:
build_dir: *build_dir
builddeps: *build_fips_deb_deps
post-cache:
- export GOOS=linux
- export GOARCH=arm64
- export NIGHTLY=true
# - export FIPS=true # TUN-7595
- export ORIGINAL_NAME=true
- make cloudflared-deb
build-deb-arm64:
build_dir: *build_dir
builddeps: *build_deb_deps
@@ -132,21 +119,7 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=arm64
- make cloudflared-deb
github-release-macos-amd64:
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:
package-windows:
build_dir: *build_dir
builddeps:
- *pinned_go
@@ -159,17 +132,25 @@ stretch: &stretch
# libmsi and libgcab are libraries the wixl binary depends on.
- libmsi-dev
- libgcab-dev
- python3-venv
pre-cache:
- 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
post-cache:
- python3 -m venv env
- . env/bin/activate
- pip install pynacl==1.4.0 pygithub==1.55
- .teamcity/package-windows.sh
- make github-windows-upload
test:
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
post-cache:
- export GOOS=linux
@@ -179,7 +160,7 @@ stretch: &stretch
- make test | gotest-to-teamcity
test-fips:
build_dir: *build_dir
builddeps: *build_deps_fips
builddeps: *build_deps_tests
pre-cache: *build_pre_cache
post-cache:
- export GOOS=linux
@@ -190,84 +171,85 @@ stretch: &stretch
- make test | gotest-to-teamcity
component-test:
build_dir: *build_dir
builddeps:
builddeps: &build_deps_component_test
- *pinned_go
- python3.7
- python3
- 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 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
- python3-venv
pre-cache-copy-paths:
- 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
# 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
- pytest component-tests -o log_cli=true --log-cli-level=INFO
# The Named Tunnel is deleted and its route unprovisioned here.
- python3 component-tests/setup.py --type cleanup
component-test-fips:
build_dir: *build_dir
builddeps:
- *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
builddeps: *build_deps_component_test
pre-cache-copy-paths:
- component-tests/requirements.txt
pre-cache: *component_test_pre_cache
post-cache: *component_test_post_cache
update-homebrew:
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
builddeps: *build_pygithub
pre-cache: *install_pygithub
post-cache:
- make github-message
build-junos:
github-release-dryrun:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- python3
- genisoimage
pre-cache:
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
- python3-dev
- libffi-dev
- python3-setuptools
- python3-pip
- python3-venv
post-cache:
- export CGO_ENABLED=0
- export GOOS=freebsd
- export GOARCH=amd64
- make cloudflared
publish-junos:
- 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
- genisoimage
- jetez
- s4cmd
pre-cache:
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
- python3-dev
- libffi-dev
- python3-setuptools
- python3-pip
- python3-venv
post-cache:
- export GOOS=freebsd
- export GOARCH=amd64
- make publish-cloudflared-junos
- 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
buster: *stretch
bullseye: *stretch
bookworm: *stretch
bookworm: *bullseye
trixie: *bullseye
+1 -1
View File
@@ -46,7 +46,7 @@
<!--Set the cloudflared bin location to the Path Environment Variable-->
<Environment Id="ENV0"
Name="PATH"
Value="[INSTALLDIR]."
Value="[INSTALLDIR]"
Permanent="no"
Part="last"
Action="create"
+27 -17
View File
@@ -3,6 +3,7 @@ package access
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"strings"
@@ -13,6 +14,7 @@ import (
"github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/stream"
"github.com/cloudflare/cloudflared/validation"
)
@@ -38,6 +40,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
if forwarder.TokenSecret != "" {
headers.Set(cfAccessClientSecretHeader, forwarder.TokenSecret)
}
headers.Set("User-Agent", userAgent)
carrier.SetBastionDest(headers, forwarder.Destination)
@@ -58,31 +61,37 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
// useful for proxying other protocols (like ssh) over websockets
// (which you can put Access in front of)
func ssh(c *cli.Context) error {
log := logger.CreateSSHLoggerFromContext(c, logger.EnableTerminalLog)
// If not running as a forwarder, disable terminal logs as it collides with the stdin/stdout of the parent process
outputTerminal := logger.DisableTerminalLog
if c.IsSet(sshURLFlag) {
outputTerminal = logger.EnableTerminalLog
}
log := logger.CreateSSHLoggerFromContext(c, outputTerminal)
// get the hostname from the cmdline and error out if its not provided
rawHostName := c.String(sshHostnameFlag)
hostname, err := validation.ValidateHostname(rawHostName)
if err != nil || rawHostName == "" {
url, err := parseURL(rawHostName)
if err != nil {
log.Err(err).Send()
return cli.ShowCommandHelp(c, "ssh")
}
originURL := ensureURLScheme(hostname)
// get the headers from the cmdline and add them
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
headers := parseRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) {
headers.Set(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
}
if c.IsSet(sshTokenSecretFlag) {
headers.Set(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag))
}
headers.Set("User-Agent", userAgent)
carrier.SetBastionDest(headers, c.String(sshDestinationFlag))
options := &carrier.StartOptions{
OriginURL: originURL,
OriginURL: url.String(),
Headers: headers,
Host: hostname,
Host: url.Host,
}
if connectTo := c.String(sshConnectTo); connectTo != "" {
@@ -121,16 +130,17 @@ func ssh(c *cli.Context) error {
return err
}
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
}
func buildRequestHeaders(values []string) http.Header {
headers := make(http.Header)
for _, valuePair := range values {
split := strings.Split(valuePair, ":")
if len(split) > 1 {
headers.Add(strings.TrimSpace(split[0]), strings.TrimSpace(split[1]))
var s io.ReadWriter
s = &carrier.StdinoutStream{}
if c.IsSet(sshDebugStream) {
maxMessages := c.Uint64(sshDebugStream)
if maxMessages == 0 {
// default to 10 if provided but unset
maxMessages = 10
}
logger := log.With().Str("host", url.Host).Logger()
s = stream.NewDebugStream(s, &logger, maxMessages)
}
return headers
carrier.StartClient(wsConn, s, options)
return nil
}
-18
View File
@@ -1,18 +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"})
assert.Equal(t, headers.Get("client"), values.Get("client"))
assert.Equal(t, headers.Get("secret"), values.Get("secret"))
}
+78 -29
View File
@@ -26,6 +26,8 @@ import (
)
const (
appURLFlag = "app"
loginQuietFlag = "quiet"
sshHostnameFlag = "hostname"
sshDestinationFlag = "destination"
sshURLFlag = "url"
@@ -34,6 +36,7 @@ const (
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
sshConnectTo = "connect-to"
sshDebugStream = "debug-stream"
sshConfigTemplate = `
Add to your {{.Home}}/.ssh/config:
@@ -81,14 +84,29 @@ func Commands() []*cli.Command {
applications from the command line.`,
Subcommands: []*cli.Command{
{
Name: "login",
Action: cliutil.Action(login),
Usage: "login <url of access application>",
Name: "login",
Action: cliutil.Action(login),
Usage: "login <url of access application>",
ArgsUsage: "url of Access application",
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.
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
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",
@@ -102,12 +120,12 @@ func Commands() []*cli.Command {
{
Name: "token",
Action: cliutil.Action(generateToken),
Usage: "token -app=<url of access application>",
Usage: "token <url of access application>",
ArgsUsage: "url of Access application",
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "app",
Name: appURLFlag,
},
},
},
@@ -123,15 +141,18 @@ func Commands() []*cli.Command {
Name: sshHostnameFlag,
Aliases: []string{"tunnel-host", "T"},
Usage: "specify the hostname of your application.",
EnvVars: []string{"TUNNEL_SERVICE_HOSTNAME"},
},
&cli.StringFlag{
Name: sshDestinationFlag,
Usage: "specify the destination address of your SSH server.",
Name: sshDestinationFlag,
Usage: "specify the destination address of your SSH server.",
EnvVars: []string{"TUNNEL_SERVICE_DESTINATION"},
},
&cli.StringFlag{
Name: sshURLFlag,
Aliases: []string{"listener", "L"},
Usage: "specify the host:port to forward data to Cloudflare edge.",
EnvVars: []string{"TUNNEL_SERVICE_URL"},
},
&cli.StringSliceFlag{
Name: sshHeaderFlag,
@@ -151,9 +172,12 @@ func Commands() []*cli.Command {
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
},
&cli.StringFlag{
Name: logger.LogSSHDirectoryFlag,
Aliases: []string{"logfile"}, //added to match the tunnel side
Usage: "Save application log to this directory for reporting issues.",
Name: logger.LogFileFlag,
Usage: "Save application log to this file for reporting issues.",
},
&cli.StringFlag{
Name: logger.LogSSHDirectoryFlag,
Usage: "Save application log to this directory for reporting issues.",
},
&cli.StringFlag{
Name: logger.LogSSHLevelFlag,
@@ -165,6 +189,11 @@ func Commands() []*cli.Command {
Hidden: true,
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
},
&cli.Uint64Flag{
Name: sshDebugStream,
Hidden: true,
Usage: "Writes up-to the max provided stream payloads to the logger as debug statements.",
},
},
},
{
@@ -212,10 +241,8 @@ func login(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
args := c.Args()
rawURL := ensureURLScheme(args.First())
appURL, err := url.Parse(rawURL)
if args.Len() < 1 || err != nil {
appURL, err := getAppURLFromArgs(c)
if err != nil {
log.Error().Msg("Please provide the url of the Access application")
return err
}
@@ -238,21 +265,22 @@ func login(c *cli.Context) error {
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
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
}
// 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
func curl(c *cli.Context) error {
err := sentry.Init(sentry.ClientOptions{
@@ -327,6 +355,17 @@ func run(cmd string, args ...string) error {
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
func generateToken(c *cli.Context) error {
err := sentry.Init(sentry.ClientOptions{
@@ -336,8 +375,8 @@ func generateToken(c *cli.Context) error {
if err != nil {
return err
}
appURL, err := url.Parse(ensureURLScheme(c.String("app")))
if err != nil || c.NumFlags() < 1 {
appURL, err := getAppURLFromArgs(c)
if err != nil {
fmt.Fprintln(os.Stderr, "Please provide a url.")
return err
}
@@ -389,7 +428,7 @@ func sshGen(c *cli.Context) error {
return cli.ShowCommandHelp(c, "ssh-gen")
}
originURL, err := url.Parse(ensureURLScheme(hostname))
originURL, err := parseURL(hostname)
if err != nil {
return err
}
@@ -468,6 +507,11 @@ func processURL(s string) (*url.URL, error) {
// cloudflaredPath pulls the full path of cloudflared on disk
func cloudflaredPath() string {
path, err := os.Executable()
if err == nil && isFileThere(path) {
return path
}
for _, p := range strings.Split(os.Getenv("PATH"), ":") {
path := fmt.Sprintf("%s/%s", p, "cloudflared")
if isFileThere(path) {
@@ -490,7 +534,7 @@ func isFileThere(candidate string) bool {
// Then makes a request to to the origin with the token to ensure it is valid.
// Returns nil if token is valid.
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) {
headers.Add(cfAccessClientIDHeader, c.String(sshTokenIDFlag))
}
@@ -525,6 +569,11 @@ func isTokenValid(options *carrier.StartOptions, log *zerolog.Logger) (bool, err
return false, errors.Wrap(err, "Could not create access request")
}
req.Header.Set("User-Agent", userAgent)
query := req.URL.Query()
query.Set("cloudflared_token_check", "true")
req.URL.RawQuery = query.Encode()
// Do not follow redirects
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
-25
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)
}
})
}
}
+55
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
}
+80
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")
})
}
+31 -1
View File
@@ -1,7 +1,10 @@
package cliutil
import (
"crypto/sha256"
"fmt"
"io"
"os"
"runtime"
"github.com/rs/zerolog"
@@ -13,6 +16,7 @@ type BuildInfo struct {
GoArch string `json:"go_arch"`
BuildType string `json:"build_type"`
CloudflaredVersion string `json:"cloudflared_version"`
Checksum string `json:"checksum"`
}
func GetBuildInfo(buildType, version string) *BuildInfo {
@@ -22,11 +26,12 @@ func GetBuildInfo(buildType, version string) *BuildInfo {
GoArch: runtime.GOARCH,
BuildType: buildType,
CloudflaredVersion: version,
Checksum: currentBinaryChecksum(),
}
}
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 != "" {
log.Info().Msgf("Built%s", bi.GetBuildTypeMsg())
}
@@ -51,3 +56,28 @@ func (bi *BuildInfo) GetBuildTypeMsg() string {
func (bi *BuildInfo) UserAgent() string {
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
}
-1
View File
@@ -1,5 +1,4 @@
//go:build !windows && !darwin && !linux
// +build !windows,!darwin,!linux
package main
+89 -32
View File
@@ -1,5 +1,4 @@
//go:build linux
// +build linux
package main
@@ -25,6 +24,9 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
Name: "install",
Usage: "Install cloudflared as a system service",
Action: cliutil.ConfiguredAction(installLinuxService),
Flags: []cli.Flag{
noUpdateServiceFlag,
},
},
{
Name: "uninstall",
@@ -39,19 +41,22 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
// The directory and files that are used by the service.
// These are hard-coded in the templates below.
const (
serviceConfigDir = "/etc/cloudflared"
serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem"
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
cloudflaredService = "cloudflared.service"
serviceConfigDir = "/etc/cloudflared"
serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem"
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
cloudflaredService = "cloudflared.service"
cloudflaredUpdateService = "cloudflared-update.service"
cloudflaredUpdateTimer = "cloudflared-update.timer"
)
var systemdTemplates = []ServiceTemplate{
{
var systemdAllTemplates = map[string]ServiceTemplate{
cloudflaredService: {
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
Content: `[Unit]
Description=cloudflared
After=network.target
After=network-online.target
Wants=network-online.target
[Service]
TimeoutStartSec=0
@@ -64,18 +69,19 @@ RestartSec=5s
WantedBy=multi-user.target
`,
},
{
Path: "/etc/systemd/system/cloudflared-update.service",
cloudflaredUpdateService: {
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateService),
Content: `[Unit]
Description=Update cloudflared
After=network.target
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
`,
},
{
Path: "/etc/systemd/system/cloudflared-update.timer",
cloudflaredUpdateTimer: {
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateTimer),
Content: `[Unit]
Description=Update cloudflared
@@ -106,7 +112,7 @@ var sysvTemplate = ServiceTemplate{
# Description: cloudflared agent
### END INIT INFO
name=$(basename $(readlink -f $0))
cmd="{{.Path}} --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
cmd="{{.Path}} --pidfile /var/run/$name.pid {{ range .ExtraArgs }} {{ . }}{{ end }}"
pid_file="/var/run/$name.pid"
stdout_log="/var/log/$name.log"
stderr_log="/var/log/$name.err"
@@ -178,6 +184,14 @@ exit 0
`,
}
var (
noUpdateServiceFlag = &cli.BoolFlag{
Name: "no-update-service",
Usage: "Disable auto-update of the cloudflared linux service, which restarts the server to upgrade for new versions.",
Value: false,
}
)
func isSystemd() bool {
if _, err := os.Stat("/run/systemd/system"); err == nil {
return true
@@ -196,6 +210,9 @@ func installLinuxService(c *cli.Context) error {
Path: etPath,
}
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
if c.NArg() == 0 {
extraArgsFunc = buildArgsForConfig
@@ -213,10 +230,10 @@ func installLinuxService(c *cli.Context) error {
switch {
case isSystemd():
log.Info().Msgf("Using Systemd")
err = installSystemd(&templateArgs, log)
err = installSystemd(&templateArgs, autoUpdate, log)
default:
log.Info().Msgf("Using SysV")
err = installSysv(&templateArgs, log)
err = installSysv(&templateArgs, autoUpdate, log)
}
if err == nil {
@@ -261,7 +278,20 @@ credentials-file: CREDENTIALS-FILE
}, nil
}
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
func installSystemd(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error {
var systemdTemplates []ServiceTemplate
if autoUpdate {
systemdTemplates = []ServiceTemplate{
systemdAllTemplates[cloudflaredService],
systemdAllTemplates[cloudflaredUpdateService],
systemdAllTemplates[cloudflaredUpdateTimer],
}
} else {
systemdTemplates = []ServiceTemplate{
systemdAllTemplates[cloudflaredService],
}
}
for _, serviceTemplate := range systemdTemplates {
err := serviceTemplate.Generate(templateArgs)
if err != nil {
@@ -273,10 +303,14 @@ func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) erro
log.Err(err).Msgf("systemctl enable %s error", cloudflaredService)
return err
}
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
log.Err(err).Msg("systemctl start cloudflared-update.timer error")
return err
if autoUpdate {
if err := runCommand("systemctl", "start", cloudflaredUpdateTimer); err != nil {
log.Err(err).Msgf("systemctl start %s error", cloudflaredUpdateTimer)
return err
}
}
if err := runCommand("systemctl", "daemon-reload"); err != nil {
log.Err(err).Msg("systemctl daemon-reload error")
return err
@@ -284,12 +318,19 @@ func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) erro
return runCommand("systemctl", "start", cloudflaredService)
}
func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
func installSysv(templateArgs *ServiceTemplateArgs, autoUpdate bool, log *zerolog.Logger) error {
confPath, err := sysvTemplate.ResolvePath()
if err != nil {
log.Err(err).Msg("error resolving system path")
return err
}
if autoUpdate {
templateArgs.ExtraArgs = append([]string{"--autoupdate-freq 24h0m0s"}, templateArgs.ExtraArgs...)
} else {
templateArgs.ExtraArgs = append([]string{"--no-autoupdate"}, templateArgs.ExtraArgs...)
}
if err := sysvTemplate.Generate(templateArgs); err != nil {
log.Err(err).Msg("error generating system template")
return err
@@ -327,19 +368,35 @@ func uninstallLinuxService(c *cli.Context) error {
}
func uninstallSystemd(log *zerolog.Logger) error {
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
return err
// Get only the installed services
installedServices := make(map[string]ServiceTemplate)
for serviceName, serviceTemplate := range systemdAllTemplates {
if err := runCommand("systemctl", "list-units", "--all", "|", "grep", serviceName); err == nil {
installedServices[serviceName] = serviceTemplate
} else {
log.Info().Msgf("Service '%s' not installed, skipping its uninstall", serviceName)
}
}
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
return err
if _, exists := installedServices[cloudflaredService]; exists {
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
return err
}
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
return err
}
}
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
log.Err(err).Msg("systemctl stop cloudflared-update.timer error")
return err
if _, exists := installedServices[cloudflaredUpdateTimer]; exists {
if err := runCommand("systemctl", "stop", cloudflaredUpdateTimer); err != nil {
log.Err(err).Msgf("systemctl stop %s error", cloudflaredUpdateTimer)
return err
}
}
for _, serviceTemplate := range systemdTemplates {
for _, serviceTemplate := range installedServices {
if err := serviceTemplate.Remove(); err != nil {
log.Err(err).Msg("error removing service template")
return err
-1
View File
@@ -1,5 +1,4 @@
//go:build darwin
// +build darwin
package main
+16 -1
View File
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"math/rand"
"os"
"strings"
"time"
@@ -49,6 +50,9 @@ var (
)
func main() {
// FIXME: TUN-8148: Disable QUIC_GO ECN due to bugs in proper detection if supported
os.Setenv("QUIC_GO_DISABLE_ECN", "1")
rand.Seed(time.Now().UnixNano())
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
maxprocs.Set()
@@ -87,7 +91,7 @@ func main() {
tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command...
access.Init(graceShutdownC, Version)
updater.Init(Version)
updater.Init(bInfo)
tracing.Init(Version)
token.Init(Version)
tail.Init(bInfo)
@@ -130,11 +134,22 @@ To determine if an update happened in a script, check for error code 11.`,
{
Name: "version",
Action: func(c *cli.Context) (err error) {
if c.Bool("short") {
fmt.Println(strings.Split(c.App.Version, " ")[0])
return nil
}
version(c)
return nil
},
Usage: versionText,
Description: versionText,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "short",
Aliases: []string{"s"},
Usage: "print just the version number",
},
},
},
}
cmds = append(cmds, tunnel.Commands()...)
+2 -3
View File
@@ -5,7 +5,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
@@ -64,7 +63,7 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
return fmt.Errorf("error creating %s: %v", plistFolder, err)
}
err = ioutil.WriteFile(resolvedPath, buffer.Bytes(), fileMode)
err = os.WriteFile(resolvedPath, buffer.Bytes(), fileMode)
if err != nil {
return fmt.Errorf("error writing %s: %v", resolvedPath, err)
}
@@ -103,7 +102,7 @@ func runCommand(command string, args ...string) error {
return fmt.Errorf("error starting %s: %v", command, err)
}
output, _ := ioutil.ReadAll(stderr)
output, _ := io.ReadAll(stderr)
err = cmd.Wait()
if err != nil {
return fmt.Errorf("%s %v returned with error code %v due to: %v", command, args, err, string(output))
+38 -5
View File
@@ -99,6 +99,12 @@ func buildTailCommand(subcommands []*cli.Command) *cli.Command {
Value: "",
EnvVars: []string{"TUNNEL_MANAGEMENT_TOKEN"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output format for the logs (default, json)",
Value: "default",
EnvVars: []string{"TUNNEL_MANAGEMENT_OUTPUT"},
},
&cli.StringFlag{
Name: "management-hostname",
Usage: "Management hostname to signify incoming management requests",
@@ -270,6 +276,24 @@ func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) {
return url.URL{Scheme: "wss", Host: managementHostname, Path: "/logs", RawQuery: query.Encode()}, nil
}
func printLine(log *management.Log, logger *zerolog.Logger) {
fields, err := json.Marshal(log.Fields)
if err != nil {
fields = []byte("unable to parse fields")
logger.Debug().Msgf("unable to parse fields from event %+v", log)
}
fmt.Printf("%s %s %s %s %s\n", log.Time, log.Level, log.Event, log.Message, fields)
}
func printJSON(log *management.Log, logger *zerolog.Logger) {
output, err := json.Marshal(log)
if err != nil {
logger.Debug().Msgf("unable to parse event to json %+v", log)
} else {
fmt.Println(string(output))
}
}
// Run implements a foreground runner
func Run(c *cli.Context) error {
log := createLogger(c)
@@ -278,6 +302,16 @@ func Run(c *cli.Context) error {
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(signals)
output := "default"
switch c.String("output") {
case "default", "":
output = "default"
case "json":
output = "json"
default:
log.Err(errors.New("invalid --output value provided, please make sure it is one of: default, json")).Send()
}
filters, err := parseFilters(c)
if err != nil {
log.Error().Err(err).Msgf("invalid filters provided")
@@ -358,12 +392,11 @@ func Run(c *cli.Context) error {
}
// Output all the logs received to stdout
for _, l := range logs.Logs {
fields, err := json.Marshal(l.Fields)
if err != nil {
fields = []byte("unable to parse fields")
log.Debug().Msgf("unable to parse fields from event %+v", l)
if output == "json" {
printJSON(l, log)
} else {
printLine(l, log)
}
fmt.Printf("%s %s %s %s %s\n", l.Time, l.Level, l.Event, l.Message, fields)
}
case management.UnknownServerEventType:
fallthrough
+245 -17
View File
@@ -4,15 +4,15 @@ import (
"bufio"
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"runtime/trace"
"strings"
"sync"
"time"
"github.com/coreos/go-systemd/daemon"
"github.com/coreos/go-systemd/v22/daemon"
"github.com/facebookgo/grace/gracenet"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
@@ -29,6 +29,7 @@ import (
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/credentials"
"github.com/cloudflare/cloudflared/diagnostic"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/features"
"github.com/cloudflare/cloudflared/ingress"
@@ -40,6 +41,7 @@ import (
"github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstate"
"github.com/cloudflare/cloudflared/validation"
)
@@ -79,6 +81,25 @@ const (
// 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"
// uiFlag is to enable launching cloudflared in interactive UI mode
uiFlag = "ui"
@@ -107,6 +128,94 @@ var (
"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)
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/)")
// 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",
"autoupdate-freq",
"no-autoupdate",
"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",
"loglevel",
"transport-loglevel",
"logfile",
"log-directory",
"trace-output",
"proxy-dns",
"proxy-dns-port",
"proxy-dns-address",
"proxy-dns-upstream",
"proxy-dns-max-upstream-conns",
"proxy-dns-bootstrap",
"is-autoupdated",
"edge",
"region",
"edge-ip-version",
"edge-bind-address",
"cacert",
"hostname",
"id",
"lb-pool",
"api-url",
"metrics-update-freq",
"tag",
"heartbeat-interval",
"heartbeat-count",
"max-edge-addr-retries",
"retries",
"ha-connections",
"rpc-timeout",
"write-stream-timeout",
"quic-disable-pmtu-discovery",
"quic-connection-level-flow-control-limit",
"quic-stream-level-flow-control-limit",
"label",
"grace-period",
"compression-quality",
"use-reconnect-token",
"dial-edge-timeout",
"stdin-control",
"name",
"ui",
"quick-service",
"max-fetch-size",
"post-quantum",
"management-diagnostics",
"protocol",
"overwrite-dns",
"help",
}
)
func Flags() []cli.Flag {
@@ -121,11 +230,13 @@ func Commands() []*cli.Command {
buildVirtualNetworkSubcommand(false),
buildRunCommand(),
buildListCommand(),
buildReadyCommand(),
buildInfoCommand(),
buildIngressSubcommand(),
buildDeleteCommand(),
buildCleanupCommand(),
buildTokenCommand(),
buildDiagCommand(),
// for compatibility, allow following as tunnel subcommands
proxydns.Command(true),
cliutil.RemovedCommand("db-connect"),
@@ -277,7 +388,7 @@ func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
func StartServer(
c *cli.Context,
info *cliutil.BuildInfo,
namedTunnel *connection.NamedTunnelProperties,
namedTunnel *connection.TunnelProperties,
log *zerolog.Logger,
) error {
err := sentry.Init(sentry.ClientOptions{
@@ -297,7 +408,7 @@ func StartServer(
}
if c.IsSet("trace-output") {
tmpTraceFile, err := ioutil.TempFile("", "trace")
tmpTraceFile, err := os.CreateTemp("", "trace")
if err != nil {
log.Err(err).Msg("Failed to create new temporary file to save trace output")
}
@@ -333,7 +444,7 @@ func StartServer(
logClientOptions(c, log)
// 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()
go waitForSignal(graceShutdownC, log)
@@ -385,7 +496,7 @@ func StartServer(
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 {
log.Err(err).Msg("Couldn't start tunnel")
return err
@@ -399,7 +510,12 @@ func StartServer(
}
}
localRules := []ingress.Rule{}
// Disable ICMP packet routing for quick tunnels
if quickTunnelURL != "" {
tunnelConfig.ICMPRouterServer = nil
}
internalRules := []ingress.Rule{}
if features.Contains(features.FeatureManagementLogs) {
serviceIP := c.String("service-op-ip")
if edgeAddrs, err := edgediscovery.ResolveEdge(log, tunnelConfig.Region, tunnelConfig.EdgeIPVersion); err == nil {
@@ -410,32 +526,56 @@ func StartServer(
mgmt := management.New(
c.String("management-hostname"),
c.Bool("management-diagnostics"),
serviceIP,
clientID,
c.String(connectorLabelFlag),
logger.ManagementLogger.Log,
logger.ManagementLogger,
)
localRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
internalRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
}
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, localRules, tunnelConfig.Log)
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, internalRules, tunnelConfig.Log)
if err != nil {
return err
}
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
metricsListener, err := metrics.CreateMetricsListener(&listeners, c.String("metrics"))
if err != nil {
log.Err(err).Msg("Error opening metrics server listener")
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
readinessServer := metrics.NewReadyServer(log, clientID)
observer.RegisterSink(readinessServer)
tracker := tunnelstate.NewConnTracker(log)
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{
ReadyServer: readinessServer,
DiagnosticHandler: diagnosticHandler,
QuickTunnelHostname: quickTunnelURL,
Orchestrator: orchestrator,
}
@@ -647,9 +787,9 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "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"},
Hidden: shouldHide,
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "heartbeat-interval",
@@ -683,6 +823,39 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Value: 4,
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: rpcTimeout,
Value: 5 * time.Second,
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: 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{
Name: quicDisablePathMTUDiscovery,
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.",
Value: false,
Hidden: true,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: 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: 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{
Name: connectorLabelFlag,
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.",
@@ -756,6 +929,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_POST_QUANTUM"},
Hidden: FipsEnabled,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "management-diagnostics",
Usage: "Enables the in-depth diagnostic routes to be made available over the management service (/debug/pprof, /metrics, etc.)",
EnvVars: []string{"TUNNEL_MANAGEMENT_DIAGNOSTICS"},
Value: true,
}),
selectProtocolFlag,
overwriteDNSFlag,
}...)
@@ -793,9 +972,15 @@ func configureCloudflaredFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
Name: "metrics",
Value: metrics.GetMetricsDefaultAddress(metrics.Runtime),
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"},
Hidden: shouldHide,
}),
@@ -1126,3 +1311,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 logger.LogDirectoryFlag, logger.LogFileFlag:
{
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
}
+3 -1
View File
@@ -4,10 +4,12 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/features"
)
func TestDedup(t *testing.T) {
expected := []string{"a", "b"}
actual := dedup([]string{"a", "b", "a"})
actual := features.Dedup([]string{"a", "b", "a"})
require.ElementsMatch(t, expected, actual)
}
+72 -73
View File
@@ -1,9 +1,9 @@
package tunnel
import (
"context"
"crypto/tls"
"fmt"
mathRand "math/rand"
"net"
"net/netip"
"os"
@@ -15,7 +15,7 @@ import (
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/config"
@@ -27,15 +27,18 @@ import (
"github.com/cloudflare/cloudflared/orchestration"
"github.com/cloudflare/cloudflared/supervisor"
"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
)
var (
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
serviceUrl = developerPortal + "/reference/service/"
argumentsUrl = developerPortal + "/reference/arguments/"
developerPortal = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup"
serviceUrl = developerPortal + "/tunnel-guide/local/as-a-service/"
argumentsUrl = developerPortal + "/tunnel-guide/local/local-management/arguments/"
secretFlags = [2]*altsrc.StringFlag{credentialsContentsFlag, tunnelTokenFlag}
@@ -105,7 +108,7 @@ func isSecretEnvVar(key string) bool {
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") &&
!(c.IsSet("name") || // adhoc-named tunnel
c.IsSet(ingress.HelloWorldFlag) || // quick or named tunnel
@@ -113,11 +116,12 @@ func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelPrope
}
func prepareTunnelConfig(
ctx context.Context,
c *cli.Context,
info *cliutil.BuildInfo,
log, logTransport *zerolog.Logger,
observer *connection.Observer,
namedTunnel *connection.NamedTunnelProperties,
namedTunnel *connection.TunnelProperties,
) (*supervisor.TunnelConfig, *orchestration.Config, error) {
clientID, err := uuid.NewRandom()
if err != nil {
@@ -129,26 +133,40 @@ func prepareTunnelConfig(
log.Err(err).Msg("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")
needPQ := c.Bool("post-quantum")
if needPQ {
clientFeatures := features.Dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
staticFeatures := features.StaticFeatures{}
if c.Bool("post-quantum") {
if FipsEnabled {
return nil, nil, fmt.Errorf("post-quantum not supported in FIPS mode")
}
pqMode := features.PostQuantumStrict
staticFeatures.PostQuantumMode = &pqMode
}
featureSelector, err := features.NewFeatureSelector(ctx, namedTunnel.Credentials.AccountTag, staticFeatures, log)
if err != nil {
return nil, nil, errors.Wrap(err, "Failed to create feature selector")
}
pqMode := featureSelector.PostQuantumMode()
if pqMode == features.PostQuantumStrict {
// Error if the user tries to force a non-quic transport protocol
if transportProtocol != connection.AutoSelectFlag && transportProtocol != connection.QUIC.String() {
return nil, nil, fmt.Errorf("post-quantum is only supported with the quic transport")
}
transportProtocol = connection.QUIC.String()
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
log.Info().Msgf(
"Using hybrid post-quantum key agreement %s",
supervisor.PQKexName,
)
}
clientFeatures := dedup(append(c.StringSlice("features"), features.DefaultFeatures...))
if needPQ {
clientFeatures = append(clientFeatures, features.FeaturePostQuantum)
}
namedTunnel.Client = tunnelpogs.ClientInfo{
namedTunnel.Client = pogs.ClientInfo{
ClientID: clientID[:],
Features: clientFeatures,
Version: info.Version(),
@@ -203,15 +221,6 @@ func prepareTunnelConfig(
log.Warn().Str("edgeIPVersion", edgeIPVersion.String()).Err(err).Msg("Overriding edge-ip-version")
}
var pqKexIdx int
if needPQ {
pqKexIdx = mathRand.Intn(len(supervisor.PQKexes))
log.Info().Msgf(
"Using experimental hybrid post-quantum key agreement %s",
supervisor.PQKexNames[supervisor.PQKexes[pqKexIdx]],
)
}
tunnelConfig := &supervisor.TunnelConfig{
GracePeriod: gracePeriod,
ReplaceExisting: c.Bool("force"),
@@ -222,7 +231,6 @@ func prepareTunnelConfig(
EdgeIPVersion: edgeIPVersion,
EdgeBindAddr: edgeBindAddr,
HAConnections: c.Int(haConnectionsFlag),
IncidentLookup: supervisor.NewIncidentLookup(),
IsAutoupdated: c.Bool("is-autoupdated"),
LBPool: c.String("lb-pool"),
Tags: tags,
@@ -231,25 +239,30 @@ func prepareTunnelConfig(
Observer: observer,
ReportedVersion: info.Version(),
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
Retries: uint(c.Int("retries")),
RunFromTerminal: isRunningFromTerminal(),
NamedTunnel: namedTunnel,
ProtocolSelector: protocolSelector,
EdgeTLSConfigs: edgeTLSConfigs,
NeedPQ: needPQ,
PQKexIdx: pqKexIdx,
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
Retries: uint(c.Int("retries")),
RunFromTerminal: isRunningFromTerminal(),
NamedTunnel: namedTunnel,
ProtocolSelector: protocolSelector,
EdgeTLSConfigs: edgeTLSConfigs,
FeatureSelector: featureSelector,
MaxEdgeAddrRetries: uint8(c.Int("max-edge-addr-retries")),
RPCTimeout: c.Duration(rpcTimeout),
WriteStreamTimeout: c.Duration(writeStreamTimeout),
DisableQUICPathMTUDiscovery: c.Bool(quicDisablePathMTUDiscovery),
QUICConnectionLevelFlowControlLimit: c.Uint64(quicConnLevelFlowControlLimit),
QUICStreamLevelFlowControlLimit: c.Uint64(quicStreamLevelFlowControlLimit),
}
packetConfig, err := newPacketConfig(c, log)
icmpRouter, err := newICMPRouter(c, log)
if err != nil {
log.Warn().Err(err).Msg("ICMP proxy feature is disabled")
} else {
tunnelConfig.PacketConfig = packetConfig
tunnelConfig.ICMPRouterServer = icmpRouter
}
orchestratorConfig := &orchestration.Config{
Ingress: &ingressRules,
WarpRouting: ingress.NewWarpRoutingConfig(&cfg.WarpRouting),
ConfigurationFlags: parseConfigFlags(c),
WriteTimeout: c.Duration(writeStreamTimeout),
}
return tunnelConfig, orchestratorConfig, nil
}
@@ -275,26 +288,7 @@ func gracePeriod(c *cli.Context) (time.Duration, error) {
}
func isRunningFromTerminal() bool {
return terminal.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
return term.IsTerminal(int(os.Stdout.Fd()))
}
// ParseConfigIPVersion returns the IP version from possible expected values from config
@@ -357,33 +351,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, ipv6Src, err := determineICMPSources(c, logger)
if err != nil {
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("icmpv4-src"), logger)
if err != nil {
return nil, errors.Wrap(err, "failed to determine IPv4 source address for ICMP proxy")
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)
ipv6Src, zone, err := determineICMPv6Src(c.String("icmpv6-src"), logger, ipv4Src)
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 != "" {
logger.Info().Msgf("ICMP proxy will use %s in zone %s as source for IPv6", ipv6Src, zone)
} else {
logger.Info().Msgf("ICMP proxy will use %s as source for IPv6", ipv6Src)
}
icmpRouter, err := ingress.NewICMPRouter(ipv4Src, ipv6Src, zone, logger)
if err != nil {
return nil, err
}
return &ingress.GlobalRouterConfig{
ICMPRouter: icmpRouter,
IPv4Src: ipv4Src,
IPv6Src: ipv6Src,
Zone: zone,
}, nil
return ipv4Src, ipv6Src, nil
}
func determineICMPv4Src(userDefinedSrc string, logger *zerolog.Logger) (netip.Addr, error) {
@@ -413,13 +413,12 @@ type interfaceIP struct {
func determineICMPv6Src(userDefinedSrc string, logger *zerolog.Logger, ipv4Src netip.Addr) (addr netip.Addr, zone string, err error) {
if userDefinedSrc != "" {
userDefinedIP, zone, _ := strings.Cut(userDefinedSrc, "%")
addr, err := netip.ParseAddr(userDefinedIP)
addr, err := netip.ParseAddr(userDefinedSrc)
if err != nil {
return netip.Addr{}, "", err
}
if addr.Is6() {
return addr, zone, nil
return addr, addr.Zone(), nil
}
return netip.Addr{}, "", fmt.Errorf("expect IPv6, but %s is IPv4", userDefinedSrc)
}
@@ -1,5 +1,4 @@
//go:build ignore
// +build ignore
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
+1 -2
View File
@@ -1,7 +1,6 @@
package tunnel
import (
"io/ioutil"
"os"
)
@@ -23,5 +22,5 @@ func (fs realFileSystem) validFilePath(path string) bool {
}
func (fs realFileSystem) readFile(filePath string) ([]byte, error) {
return ioutil.ReadFile(filePath)
return os.ReadFile(filePath)
}
@@ -139,7 +139,7 @@ func testURLCommand(c *cli.Context) error {
}
_, 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())
return nil
}
+1 -2
View File
@@ -2,7 +2,6 @@ package tunnel
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
@@ -66,7 +65,7 @@ func login(c *cli.Context) error {
return err
}
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
if err := os.WriteFile(path, resourceData, 0600); err != nil {
return errors.Wrap(err, fmt.Sprintf("error writing cert to %s", path))
}
+20 -7
View File
@@ -3,6 +3,7 @@ package tunnel
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
@@ -15,10 +16,7 @@ import (
const httpTimeout = 15 * time.Second
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. 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"
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"
// RunQuickTunnel requests a tunnel from the specified service.
// We use this to power quick tunnels on trycloudflare.com, but the
@@ -35,14 +33,29 @@ func RunQuickTunnel(sc *subcommandContext) error {
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 {
return errors.Wrap(err, "failed to request quick Tunnel")
}
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
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")
}
@@ -79,7 +92,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
return StartServer(
sc.c,
buildInfo,
&connection.NamedTunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
&connection.TunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
sc.log,
)
}
-1
View File
@@ -1,5 +1,4 @@
//go:build !windows
// +build !windows
package tunnel
+3 -8
View File
@@ -156,7 +156,7 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
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("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("The delete tunnel error is: %v", deleteErr))
} else {
@@ -206,13 +206,8 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
if !tunnel.DeletedAt.IsZero() {
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)
}
@@ -266,7 +261,7 @@ func (sc *subcommandContext) runWithCredentials(credentials connection.Credentia
return StartServer(
sc.c,
buildInfo,
&connection.NamedTunnelProperties{Credentials: credentials},
&connection.TunnelProperties{Credentials: credentials},
sc.log,
)
}
@@ -1,6 +1,9 @@
package tunnel
import (
"net"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/cfapi"
@@ -24,12 +27,12 @@ func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, err
return client.AddRoute(newRoute)
}
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
func (sc *subcommandContext) deleteRoute(id uuid.UUID) error {
client, err := sc.client()
if err != nil {
return errors.Wrap(err, noClientMsg)
}
return client.DeleteRoute(params)
return client.DeleteRoute(id)
}
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)
}
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
}
@@ -219,7 +219,7 @@ func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, erro
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]
if !ok {
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
+174 -14
View File
@@ -5,7 +5,8 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
@@ -27,16 +28,26 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/diagnostic"
"github.com/cloudflare/cloudflared/metrics"
)
const (
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
connsSortByOptions = "id, startedAt, numConnections, version"
CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file"
CredContentsFlag = "credentials-contents"
TunnelTokenFlag = "token"
overwriteDNSFlagName = "overwrite-dns"
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
connsSortByOptions = "id, startedAt, numConnections, version"
CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file"
CredContentsFlag = "credentials-contents"
TunnelTokenFlag = "token"
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"
metricsFlagName = "metrics"
LogFieldTunnelID = "tunnelID"
)
@@ -120,8 +131,8 @@ var (
forceDeleteFlag = &cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Cleans up any stale connections before the tunnel is deleted. cloudflared will not " +
"delete a tunnel with connections without this flag.",
Usage: "Deletes a tunnel even if tunnel is connected and it has dependencies associated to it. (eg. IP routes)." +
" It is not possible to delete tunnels that have connections or non-deleted dependencies, without this flag.",
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
}
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
@@ -178,6 +189,46 @@ var (
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"},
}
metricsFlag = &cli.StringFlag{
Name: metricsFlagName,
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,
}
)
func buildCreateCommand() *cli.Command {
@@ -241,7 +292,7 @@ func writeTunnelCredentials(filePath string, credentials *connection.Credentials
if err != nil {
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
}
return ioutil.WriteFile(filePath, body, 400)
return os.WriteFile(filePath, body, 0400)
}
func buildListCommand() *cli.Command {
@@ -374,7 +425,6 @@ func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected
}
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
// Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections))
for _, connection := range connections {
@@ -398,6 +448,39 @@ func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected boo
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("metrics")
if !c.IsSet("metrics") {
return fmt.Errorf("--metrics has to be provided")
}
requestURL := fmt.Sprintf("http://%s/ready", metricsOpts)
res, err := http.Get(requestURL)
if err != nil {
return err
}
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 {
return &cli.Command{
Name: "info",
@@ -863,8 +946,10 @@ func lbRouteFromArg(c *cli.Context) (cfapi.HostnameRoute, error) {
return cfapi.NewLBRoute(lbName, lbPool), nil
}
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
var hostNameRegex = regexp.MustCompile("^[*_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
var (
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 {
if allowWildcardSubdomain {
@@ -952,3 +1037,78 @@ SUBCOMMAND OPTIONS:
`
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(metricsFlagName),
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
}
+6 -6
View File
@@ -4,23 +4,23 @@ import (
"fmt"
"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 values to printable characters (what is recognised as data in an HTTP header value).
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)
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) {
var tagSlice []tunnelpogs.Tag
func NewTagSliceFromCLI(tags []string) ([]pogs.Tag, error) {
var tagSlice []pogs.Tag
for _, compoundTag := range tags {
if tag, ok := NewTagFromCLI(compoundTag); ok {
tagSlice = append(tagSlice, tag)
+5 -5
View File
@@ -3,7 +3,7 @@ package tunnel
import (
"testing"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/stretchr/testify/assert"
)
@@ -11,12 +11,12 @@ import (
func TestSingleTag(t *testing.T) {
testCases := []struct {
Input string
Output tunnelpogs.Tag
Output pogs.Tag
Fail bool
}{
{Input: "x=y", Output: tunnelpogs.Tag{Name: "x", Value: "y"}},
{Input: "More-Complex=Tag Values", Output: tunnelpogs.Tag{Name: "More-Complex", Value: "Tag Values"}},
{Input: "First=Equals=Wins", Output: tunnelpogs.Tag{Name: "First", Value: "Equals=Wins"}},
{Input: "x=y", Output: pogs.Tag{Name: "x", Value: "y"}},
{Input: "More-Complex=Tag Values", Output: pogs.Tag{Name: "More-Complex", Value: "Tag Values"}},
{Input: "First=Equals=Wins", Output: pogs.Tag{Name: "First", Value: "Equals=Wins"}},
{Input: "x=", Fail: true},
{Input: "=y", Fail: true},
{Input: "=", Fail: true},
+24 -21
View File
@@ -21,6 +21,8 @@ var (
Aliases: []string{"vn"},
Usage: "The ID or name of the virtual network to which the route is associated to.",
}
routeAddError = errors.New("You must supply exactly one argument, the ID or CIDR of the route you want to delete")
)
func buildRouteIPSubcommand() *cli.Command {
@@ -68,11 +70,9 @@ which virtual network's routing table you want to add the route to with:
Name: "delete",
Action: cliutil.ConfiguredAction(deleteRouteCommand),
Usage: "Delete a row from your organization's private routing table",
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]",
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network
will no longer be reachable by the WARP clients. Note that if you use virtual
networks, then you have to tell which virtual network whose routing table you
have a row deleted from.`,
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [Route ID or CIDR]",
Description: `Deletes the row for the given route ID from your routing table. That portion of your network
will no longer be reachable.`,
Flags: []cli.Flag{vnetFlag},
},
{
@@ -187,33 +187,36 @@ func deleteRouteCommand(c *cli.Context) error {
}
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 routeAddError
}
_, network, err := net.ParseCIDR(c.Args().First())
var routeId uuid.UUID
routeId, err = uuid.Parse(c.Args().First())
if err != nil {
return errors.Wrap(err, "Invalid network CIDR")
}
if network == nil {
return errors.New("Invalid network CIDR")
}
_, network, err := net.ParseCIDR(c.Args().First())
if err != nil || network == nil {
return routeAddError
}
params := cfapi.DeleteRouteParams{
Network: *network,
}
var vnetId *uuid.UUID
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) {
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
routeId, err = sc.getRouteId(*network, vnetId)
if err != nil {
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")
}
fmt.Printf("Successfully deleted route for %s\n", network)
fmt.Printf("Successfully deleted route with ID %s\n", routeId)
return nil
}
@@ -269,7 +272,7 @@ func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
defer writer.Flush()
// 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
for _, route := range routes {
+23 -31
View File
@@ -12,8 +12,9 @@ import (
"github.com/facebookgo/grace/gracenet"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
@@ -31,7 +32,7 @@ const (
)
var (
version string
buildInfo *cliutil.BuildInfo
BuiltForPackageManager = ""
)
@@ -81,8 +82,8 @@ func (uo *UpdateOutcome) noUpdate() bool {
return uo.Error == nil && uo.Updated == false
}
func Init(v string) {
version = v
func Init(info *cliutil.BuildInfo) {
buildInfo = info
}
func CheckForUpdate(options updateOptions) (CheckResult, error) {
@@ -100,11 +101,12 @@ func CheckForUpdate(options updateOptions) (CheckResult, error) {
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})
return s.Check()
}
func encodeWindowsPath(path string) string {
// We do this because Windows allows spaces in directories such as
// Program Files but does not allow these directories to be spaced in batch files.
@@ -196,10 +198,9 @@ func loggedUpdate(log *zerolog.Logger, options updateOptions) UpdateOutcome {
// AutoUpdater periodically checks for new version of cloudflared.
type AutoUpdater struct {
configurable *configurable
listeners *gracenet.Net
updateConfigChan chan *configurable
log *zerolog.Logger
configurable *configurable
listeners *gracenet.Net
log *zerolog.Logger
}
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
@@ -210,10 +211,9 @@ type configurable struct {
func NewAutoUpdater(updateDisabled bool, freq time.Duration, listeners *gracenet.Net, log *zerolog.Logger) *AutoUpdater {
return &AutoUpdater{
configurable: createUpdateConfig(updateDisabled, freq, log),
listeners: listeners,
updateConfigChan: make(chan *configurable),
log: log,
configurable: createUpdateConfig(updateDisabled, freq, log),
listeners: listeners,
log: log,
}
}
@@ -232,12 +232,20 @@ 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 {
ticker := time.NewTicker(a.configurable.freq)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
updateOutcome := loggedUpdate(a.log, updateOptions{updateDisabled: !a.configurable.enabled})
if updateOutcome.Updated {
Init(updateOutcome.Version)
buildInfo.CloudflaredVersion = updateOutcome.Version
if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.log.Info().Msg("Restarting service managed by SysV...")
@@ -254,25 +262,9 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
} else if 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 {
if !supportAutoUpdate(log) {
return false
@@ -304,7 +296,7 @@ func wasInstalledFromPackageManager() bool {
}
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
return term.IsTerminal(int(os.Stdout.Fd()))
}
func IsSysV() bool {
+6
View File
@@ -9,8 +9,14 @@ import (
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
)
func init() {
Init(cliutil.GetBuildInfo("TEST", "TEST"))
}
func TestDisabledAutoUpdater(t *testing.T) {
listeners := &gracenet.Net{}
log := zerolog.Nop()
@@ -3,6 +3,7 @@ package updater
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime"
)
@@ -56,6 +57,9 @@ func (s *WorkersService) Check() (CheckResult, error) {
}
req, err := http.NewRequest(http.MethodGet, s.url, nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add(OSKeyName, runtime.GOOS)
q.Add(ArchitectureKeyName, runtime.GOARCH)
@@ -76,6 +80,10 @@ func (s *WorkersService) Check() (CheckResult, error) {
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("unable to check for update: %d", resp.StatusCode)
}
var v VersionResponse
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return nil, err
@@ -1,5 +1,4 @@
//go:build !windows
// +build !windows
package updater
@@ -11,7 +10,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
@@ -224,7 +222,7 @@ func TestUpdateService(t *testing.T) {
require.Equal(t, v.Version(), mostRecentVersion)
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
dat, err := os.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), mostRecentVersion)
@@ -243,7 +241,7 @@ func TestBetaUpdateService(t *testing.T) {
require.Equal(t, v.Version(), mostRecentBetaVersion)
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
dat, err := os.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), mostRecentBetaVersion)
@@ -289,7 +287,7 @@ func TestForcedUpdateService(t *testing.T) {
require.Equal(t, v.Version(), mostRecentVersion)
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
dat, err := os.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), mostRecentVersion)
@@ -309,7 +307,7 @@ func TestUpdateSpecificVersionService(t *testing.T) {
require.Equal(t, reqVersion, v.Version())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
dat, err := os.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, reqVersion, string(dat))
@@ -328,7 +326,7 @@ func TestCompressedUpdateService(t *testing.T) {
require.Equal(t, "2020.09.02", v.Version())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
dat, err := os.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, "2020.09.02", string(dat))
+24 -25
View File
@@ -3,7 +3,6 @@ package updater
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
@@ -16,6 +15,10 @@ import (
"strings"
"text/template"
"time"
"github.com/getsentry/sentry-go"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
)
const (
@@ -27,9 +30,9 @@ const (
// start the service
// exit with code 0 if we've reached this point indicating success.
windowsUpdateCommandTemplate = `sc stop cloudflared >nul 2>&1
del "{{.OldPath}}"
rename "{{.TargetPath}}" {{.OldName}}
rename "{{.NewPath}}" {{.BinaryName}}
del "{{.OldPath}}"
sc start cloudflared >nul 2>&1
exit /b 0`
batchFileName = "cfd_update.bat"
@@ -86,8 +89,25 @@ func (v *WorkersVersion) Apply() error {
return err
}
// check that the file is what is expected
if err := isValidChecksum(v.checksum, newFilePath); err != nil {
downloadSum, err := cliutil.FileChecksum(newFilePath)
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
}
@@ -189,27 +209,6 @@ func isCompressedFile(urlstring string) bool {
return strings.HasSuffix(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
// see the dicussion on why it has to be done this way
func writeBatchFile(targetPath string, newPath string, oldPath string) error {
-1
View File
@@ -1,5 +1,4 @@
//go:build windows
// +build windows
package main
+5 -5
View File
@@ -1,9 +1,9 @@
# Requirements
1. Python 3.7 or later with packages in the given `requirements.txt`
- E.g. with conda:
- `conda create -n component-tests python=3.7`
- `conda activate component-tests`
- `pip3 install -r requirements.txt`
1. Python 3.10 or later with packages in the given `requirements.txt`
- E.g. with venv:
- `python3 -m venv ./.venv`
- `source ./.venv/bin/activate`
- `python3 -m pip install -r requirements.txt`
2. Create a config yaml file, for example:
```
+2 -2
View File
@@ -1,8 +1,8 @@
cloudflare==2.8.15
cloudflare==2.14.3
flaky==3.7.0
pytest==7.3.1
pytest-asyncio==0.21.0
pyyaml==5.4.1
pyyaml==6.0.1
requests==2.28.2
retrying==1.3.4
websockets==11.0.1
+2 -2
View File
@@ -74,7 +74,7 @@ def delete_tunnel(config):
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
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(
config["zone_tag"],
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)
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"]
dns_records = cf.zones.dns_records.get(zone_tag, params={'name': hostname})
if len(dns_records) > 0:
+6 -6
View File
@@ -36,17 +36,17 @@ class TestConfig:
_ = start_cloudflared(tmp_path, config, validate_args)
self.match_rule(tmp_path, config,
"http://example.com/index.html", 1)
"http://example.com/index.html", 0)
self.match_rule(tmp_path, config,
"https://example.com/index.html", 1)
"https://example.com/index.html", 0)
self.match_rule(tmp_path, config,
"https://api.example.com/login", 2)
"https://api.example.com/login", 1)
self.match_rule(tmp_path, config,
"https://wss.example.com", 3)
"https://wss.example.com", 2)
self.match_rule(tmp_path, config,
"https://ssh.example.com", 4)
"https://ssh.example.com", 3)
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
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python
import requests
from conftest import CfdModes
from constants import METRICS_PORT, MAX_RETRIES, BACKOFF_SECS
from retrying import retry
from cli import CloudflaredCli
from util import LOGGER, write_config, start_cloudflared, wait_tunnel_ready, send_requests
import platform
"""
Each test in TestManagement will:
1. Acquire a management token from Cloudflare public API
2. Make a request against the management service for the running tunnel
"""
class TestManagement:
"""
test_get_host_details does the following:
1. It gets a management token from Tunnelstore using cloudflared tail token <tunnel_id>
2. It gets the connector_id after starting a cloudflare tunnel
3. It sends a request to the management host with the connector_id and management token
4. Asserts that the response has a hostname and ip.
"""
def test_get_host_details(self, tmp_path, component_tests_config):
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config)
headers = {}
headers["Content-Type"] = "application/json"
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
connector_id = cfd_cli.get_connector_id(config)[0]
url = cfd_cli.get_management_url("host_details", config, config_path)
resp = send_request(url, headers=headers)
# Assert response json.
assert resp.status_code == 200, "Expected cloudflared to return 200 for host details"
assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname"
assert resp.json()["ip"] != "", "Expected cloudflared to return ip"
assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id"
"""
test_get_metrics will verify that the /metrics endpoint returns the prometheus metrics dump
"""
def test_get_metrics(self, tmp_path, component_tests_config):
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("metrics", config, config_path)
resp = send_request(url)
# Assert response.
assert resp.status_code == 200, "Expected cloudflared to return 200 for /metrics"
assert "# HELP build_info Build and version information" in resp.text, "Expected /metrics to have with the build_info details"
"""
test_get_pprof_heap will verify that the /debug/pprof/heap endpoint returns a pprof/heap dump response
"""
def test_get_pprof_heap(self, tmp_path, component_tests_config):
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("debug/pprof/heap", config, config_path)
resp = send_request(url)
# Assert response.
assert resp.status_code == 200, "Expected cloudflared to return 200 for /debug/pprof/heap"
assert resp.headers["Content-Type"] == "application/octet-stream", "Expected /debug/pprof/heap to have return a binary response"
"""
test_get_metrics_when_disabled will verify that diagnostic endpoints (such as /metrics) return 404 and are unmounted.
"""
def test_get_metrics_when_disabled(self, tmp_path, component_tests_config):
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config)
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--management-diagnostics=false"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
url = cfd_cli.get_management_url("metrics", config, config_path)
resp = send_request(url)
# Assert response.
assert resp.status_code == 404, "Expected cloudflared to return 404 for /metrics"
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
def send_request(url, headers={}):
with requests.Session() as s:
return s.get(url, timeout=BACKOFF_SECS, headers=headers)
+3
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python
from conftest import CfdModes
from constants import METRICS_PORT
import time
from util import LOGGER, start_cloudflared, wait_tunnel_ready, get_quicktunnel_url, send_requests
class TestQuickTunnels:
@@ -9,6 +10,7 @@ class TestQuickTunnels:
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
time.sleep(10)
url = get_quicktunnel_url()
send_requests(url, 3, True)
@@ -17,6 +19,7 @@ class TestQuickTunnels:
LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
wait_tunnel_ready(require_min_connections=1)
time.sleep(10)
url = get_quicktunnel_url()
send_requests(url+"/ready", 3, True)
+8 -2
View File
@@ -47,7 +47,12 @@ class TestTail:
url = cfd_cli.get_management_wsurl("logs", config, config_path)
async with connect(url, open_timeout=5, close_timeout=5) as websocket:
# 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
await generate_and_validate_http_events(websocket, config.get_url(), 10)
# send stop_streaming
@@ -99,7 +104,8 @@ class TestTail:
await websocket.send(json.dumps({
"type": "start_streaming",
"filters": {
"sampling": 0.5
"sampling": 0.5,
"events": ["http"]
}
}))
# don't expect any http logs
+16 -7
View File
@@ -45,9 +45,10 @@ class TestTermination:
with connected:
connected.wait(self.timeout)
# Send signal after the SSE connection is established
self.terminate_by_signal(cloudflared, signal)
self.wait_eyeball_thread(
in_flight_req, self.grace_period + self.timeout)
with self.within_grace_period():
self.terminate_by_signal(cloudflared, signal)
self.wait_eyeball_thread(
in_flight_req, self.grace_period + self.timeout)
# test cloudflared terminates before grace period expires when all eyeball
# connections are drained
@@ -66,7 +67,7 @@ class TestTermination:
with connected:
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
self.terminate_by_signal(cloudflared, signal)
self.wait_eyeball_thread(in_flight_req, self.grace_period)
@@ -78,7 +79,7 @@ class TestTermination:
with start_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())
with self.within_grace_period():
with self.within_grace_period(has_connection=False):
self.terminate_by_signal(cloudflared, signal)
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
@contextmanager
def within_grace_period(self):
def within_grace_period(self, has_connection=True):
try:
start = time.time()
yield
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
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):
expected_terminate_message = "502 Bad Gateway"
-32
View File
@@ -16,38 +16,6 @@ class TestTunnel:
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=1)
"""
test_get_host_details does the following:
1. It gets a management token from Tunnelstore using cloudflared tail token <tunnel_id>
2. It gets the connector_id after starting a cloudflare tunnel
3. It sends a request to the management host with the connector_id and management token
4. Asserts that the response has a hostname and ip.
"""
def test_get_host_details(self, tmp_path, component_tests_config):
# TUN-7377 : wait_tunnel_ready does not work properly in windows.
# Skipping this test for windows for now and will address it as part of tun-7377
if platform.system() == "Windows":
return
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config)
headers = {}
headers["Content-Type"] = "application/json"
config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER)
connector_id = cfd_cli.get_connector_id(config)[0]
url = cfd_cli.get_management_url("host_details", config, config_path)
resp = send_request(url, headers=headers)
# Assert response json.
assert resp.status_code == 200, "Expected cloudflared to return 200 for host details"
assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname"
assert resp.json()["ip"] != "", "Expected cloudflared to return ip"
assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id"
def test_tunnel_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
+8 -1
View File
@@ -4,6 +4,7 @@ import platform
import subprocess
from contextlib import contextmanager
from time import sleep
import sys
import pytest
@@ -14,8 +15,14 @@ from retrying import retry
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):
return pytest.mark.skipif(
+2 -1
View File
@@ -205,6 +205,8 @@ type OriginRequestConfig struct {
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
// Hostname on the origin server certificate.
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.
// This option should be used only if your certificate is not signed by Cloudflare.
CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
@@ -257,7 +259,6 @@ type Configuration struct {
}
type WarpRoutingConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout,omitempty"`
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive,omitempty"`
}
-1
View File
@@ -23,7 +23,6 @@ func TestConfigFileSettings(t *testing.T) {
Service: "https://localhost:8001",
}
warpRouting = WarpRoutingConfig{
Enabled: true,
ConnectTimeout: &CustomDuration{Duration: 2 * time.Second},
TCPKeepAlive: &CustomDuration{Duration: 10 * time.Second},
}
+16 -4
View File
@@ -36,14 +36,20 @@ var (
flushableContentTypes = []string{sseContentType, grpcContentType}
)
// 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 {
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
GetConfigJSON() ([]byte, error)
GetOriginProxy() (OriginProxy, error)
WarpRoutingEnabled() (enabled bool)
}
type NamedTunnelProperties struct {
type TunnelProperties struct {
Credentials Credentials
Client pogs.ClientInfo
QuickTunnelUrl string
@@ -157,14 +163,16 @@ type ReadWriteAcker interface {
type HTTPResponseReadWriteAcker struct {
r io.Reader
w ResponseWriter
f http.Flusher
req *http.Request
}
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
func NewHTTPResponseReadWriterAcker(w ResponseWriter, req *http.Request) *HTTPResponseReadWriteAcker {
func NewHTTPResponseReadWriterAcker(w ResponseWriter, flusher http.Flusher, req *http.Request) *HTTPResponseReadWriteAcker {
return &HTTPResponseReadWriteAcker{
r: req.Body,
w: w,
f: flusher,
req: req,
}
}
@@ -174,7 +182,11 @@ func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
}
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
return h.w.Write(p)
n, err := h.w.Write(p)
if n > 0 {
h.f.Flush()
}
return n, err
}
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
+3 -2
View File
@@ -130,7 +130,8 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
}
wsCtx, cancel := context.WithCancel(r.Context())
readPipe, writePipe := io.Pipe()
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
go func() {
select {
case <-wsCtx.Done():
@@ -175,7 +176,7 @@ func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
}
wsCtx, cancel := context.WithCancel(r.Context())
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, w.(http.Flusher), r), &log)
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
+56 -33
View File
@@ -6,25 +6,27 @@ import (
"net"
"time"
"github.com/rs/zerolog"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/management"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
// RPCClientFunc 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
// registerClient derives a named tunnel rpc client that can then be used to register and unregister connections.
type registerClientFunc func(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient
type controlStream struct {
observer *Observer
connectedFuse ConnectedFuse
namedTunnelProperties *NamedTunnelProperties
connIndex uint8
edgeAddress net.IP
protocol Protocol
connectedFuse ConnectedFuse
tunnelProperties *TunnelProperties
connIndex uint8
edgeAddress net.IP
protocol Protocol
newRPCClientFunc RPCClientFunc
registerClientFunc registerClientFunc
registerTimeout time.Duration
gracefulShutdownC <-chan struct{}
gracePeriod time.Duration
@@ -47,27 +49,29 @@ type TunnelConfigJSONGetter interface {
func NewControlStream(
observer *Observer,
connectedFuse ConnectedFuse,
namedTunnelConfig *NamedTunnelProperties,
tunnelProperties *TunnelProperties,
connIndex uint8,
edgeAddress net.IP,
newRPCClientFunc RPCClientFunc,
registerClientFunc registerClientFunc,
registerTimeout time.Duration,
gracefulShutdownC <-chan struct{},
gracePeriod time.Duration,
protocol Protocol,
) ControlStreamHandler {
if newRPCClientFunc == nil {
newRPCClientFunc = newRegistrationRPCClient
if registerClientFunc == nil {
registerClientFunc = tunnelrpc.NewRegistrationClient
}
return &controlStream{
observer: observer,
connectedFuse: connectedFuse,
namedTunnelProperties: namedTunnelConfig,
newRPCClientFunc: newRPCClientFunc,
connIndex: connIndex,
edgeAddress: edgeAddress,
gracefulShutdownC: gracefulShutdownC,
gracePeriod: gracePeriod,
protocol: protocol,
observer: observer,
connectedFuse: connectedFuse,
tunnelProperties: tunnelProperties,
registerClientFunc: registerClientFunc,
registerTimeout: registerTimeout,
connIndex: connIndex,
edgeAddress: edgeAddress,
gracefulShutdownC: gracefulShutdownC,
gracePeriod: gracePeriod,
protocol: protocol,
}
}
@@ -77,50 +81,69 @@ func (c *controlStream) ServeControlStream(
connOptions *tunnelpogs.ConnectionOptions,
tunnelConfigGetter TunnelConfigJSONGetter,
) 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 {
rpcClient.Close()
return err
defer registrationClient.Close()
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.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location)
c.observer.sendConnectedEvent(c.connIndex, c.protocol, registrationDetails.Location, c.edgeAddress)
c.connectedFuse.Connected()
// if conn index is 0 and tunnel is not remotely managed, then send local ingress rules configuration
if c.connIndex == 0 && !registrationDetails.TunnelIsRemotelyManaged {
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.metrics.localConfigMetrics.pushes.Inc()
} else {
c.observer.log.Err(err).Msg("failed to obtain current configuration")
}
}
c.waitForUnregister(ctx, rpcClient)
return nil
return c.waitForUnregister(ctx, registrationClient)
}
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
defer rpcClient.Close()
defer registrationClient.Close()
var shutdownError error
select {
case <-ctx.Done():
shutdownError = ctx.Err()
break
case <-c.gracefulShutdownC:
c.stoppedGracefully = true
}
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().
Int(management.EventTypeKey, int(management.Cloudflared)).
Uint8(LogFieldConnIndex, c.connIndex).
IPAddr(LogFieldIPAddress, c.edgeAddress).
Msg("Unregistered tunnel connection")
return shutdownError
}
func (c *controlStream) IsStopped() bool {
-3
View File
@@ -2,7 +2,6 @@ package connection
import (
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
@@ -71,8 +70,6 @@ func isHandshakeErrRecoverable(err error, connIndex uint8, observer *Observer) b
switch err.(type) {
case edgediscovery.DialError:
log.Error().Msg("Connection unable to dial edge")
case h2mux.MuxerHandshakeError:
log.Error().Msg("Connection handshake with edge server failed")
default:
log.Error().Msg("Connection failed")
return false
+8 -5
View File
@@ -1,12 +1,15 @@
package connection
import "net"
// Event is something that happened to a connection, e.g. disconnection or registration.
type Event struct {
Index uint8
EventType Status
Location string
Protocol Protocol
URL string
Index uint8
EventType Status
Location string
Protocol Protocol
URL string
EdgeAddress net.IP
}
// Status is the status of a connection.
-32
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,
}
}
-128
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-")
}
-642
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)
}
}
+12 -7
View File
@@ -7,17 +7,15 @@ import (
"strings"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/h2mux"
)
var (
// h2mux-style special headers
// internal special headers
RequestUserHeaders = "cf-cloudflared-request-headers"
ResponseUserHeaders = "cf-cloudflared-response-headers"
ResponseMetaHeader = "cf-cloudflared-response-meta"
// h2mux-style special headers
// internal special headers
CanonicalResponseUserHeaders = http.CanonicalHeaderKey(ResponseUserHeaders)
CanonicalResponseMetaHeader = http.CanonicalHeaderKey(ResponseMetaHeader)
)
@@ -28,6 +26,13 @@ var (
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
)
// HTTPHeader is a custom header struct that expects only ever one value for the header.
// This structure is used to serialize the headers and attach them to the HTTP2 request when proxying.
type HTTPHeader struct {
Name string
Value string
}
type responseMetaHeader struct {
Source string `json:"src"`
}
@@ -104,10 +109,10 @@ func SerializeHeaders(h1Headers http.Header) string {
}
// Deserialize headers serialized by `SerializeHeader`
func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) {
func DeserializeHeaders(serializedHeaders string) ([]HTTPHeader, error) {
const unableToDeserializeErr = "Unable to deserialize headers"
var deserialized []h2mux.Header
var deserialized []HTTPHeader
for _, serializedPair := range strings.Split(serializedHeaders, ";") {
if len(serializedPair) == 0 {
continue
@@ -130,7 +135,7 @@ func DeserializeHeaders(serializedHeaders string) ([]h2mux.Header, error) {
return nil, errors.Wrap(err, unableToDeserializeErr)
}
deserialized = append(deserialized, h2mux.Header{
deserialized = append(deserialized, HTTPHeader{
Name: string(deserializedName),
Value: string(deserializedValue),
})
+26 -4
View File
@@ -46,18 +46,40 @@ func TestSerializeHeaders(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 13, len(deserializedHeaders))
h2muxExpectedHeaders := stdlibHeaderToH2muxHeader(mockHeaders)
expectedHeaders := headerToReqHeader(mockHeaders)
sort.Sort(ByName(deserializedHeaders))
sort.Sort(ByName(h2muxExpectedHeaders))
sort.Sort(ByName(expectedHeaders))
assert.True(
t,
reflect.DeepEqual(h2muxExpectedHeaders, deserializedHeaders),
fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, h2muxExpectedHeaders),
reflect.DeepEqual(expectedHeaders, deserializedHeaders),
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) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
+6 -6
View File
@@ -40,8 +40,6 @@ type HTTP2Connection struct {
connOptions *tunnelpogs.ConnectionOptions
observer *Observer
connIndex uint8
// newRPCClientFunc allows us to mock RPCs during testing
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
log *zerolog.Logger
activeRequestsWG sync.WaitGroup
@@ -69,7 +67,6 @@ func NewHTTP2Connection(
connOptions: connOptions,
observer: observer,
connIndex: connIndex,
newRPCClientFunc: newRegistrationRPCClient,
controlStreamHandler: controlStreamHandler,
log: log,
}
@@ -142,7 +139,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
break
}
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
rws := NewHTTPResponseReadWriterAcker(respWriter, respWriter, r)
requestErr = originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
Dest: host,
CFRay: FindCfRayHeader(r),
@@ -289,6 +286,10 @@ func (rp *http2RespWriter) Header() http.Header {
return rp.respHeaders
}
func (rp *http2RespWriter) Flush() {
rp.flusher.Flush()
}
func (rp *http2RespWriter) WriteHeader(status int) {
if rp.hijacked() {
rp.log.Warn().Msg("WriteHeader after hijack")
@@ -384,8 +385,7 @@ func determineHTTP2Type(r *http.Request) Type {
func handleMissingRequestParts(connType Type, r *http.Request) {
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
// for proxying. We use the same values as we used to in h2mux. For proxying they should not matter since we
// control the dialer on every egress proxied.
// for proxying. For proxying they should not matter since we control the dialer on every egress proxied.
if len(r.URL.Scheme) == 0 {
r.URL.Scheme = "http"
}
+23 -19
View File
@@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
@@ -21,8 +20,8 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
var (
@@ -37,10 +36,11 @@ func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
controlStream := NewControlStream(
obs,
mockConnectedFuse{},
&NamedTunnelProperties{},
&TunnelProperties{},
connIndex,
nil,
nil,
1*time.Second,
nil,
1*time.Second,
HTTP2,
@@ -84,7 +84,7 @@ func TestHTTP2ConfigurationSet(t *testing.T) {
resp, err := edgeHTTP2Conn.RoundTrip(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
bdy, err := ioutil.ReadAll(resp.Body)
bdy, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy))
cancel()
@@ -149,7 +149,7 @@ func TestServeHTTP(t *testing.T) {
require.NoError(t, err)
require.Equal(t, test.expectedStatus, resp.StatusCode)
if test.expectedBody != nil {
respBody, err := ioutil.ReadAll(resp.Body)
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, test.expectedBody, respBody)
}
@@ -169,31 +169,32 @@ type mockNamedTunnelRPCClient 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
}
func (mc mockNamedTunnelRPCClient) RegisterConnection(
c context.Context,
properties *NamedTunnelProperties,
options *tunnelpogs.ConnectionOptions,
ctx context.Context,
auth pogs.TunnelAuth,
tunnelID uuid.UUID,
options *pogs.ConnectionOptions,
connIndex uint8,
edgeAddress net.IP,
observer *Observer,
) (*tunnelpogs.ConnectionDetails, error) {
) (*pogs.ConnectionDetails, error) {
if mc.shouldFail != nil {
return nil, mc.shouldFail
}
close(mc.registered)
return &tunnelpogs.ConnectionDetails{
return &pogs.ConnectionDetails{
Location: "LIS",
UUID: uuid.New(),
TunnelIsRemotelyManaged: false,
}, 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)
return nil
}
func (mockNamedTunnelRPCClient) Close() {}
@@ -204,8 +205,8 @@ type mockRPCClientFactory struct {
unregistered chan struct{}
}
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient {
return mockNamedTunnelRPCClient{
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, time.Duration) tunnelrpc.RegistrationClient {
return &mockNamedTunnelRPCClient{
shouldFail: mf.shouldFail,
registered: mf.registered,
unregistered: mf.unregistered,
@@ -361,10 +362,11 @@ func TestServeControlStream(t *testing.T) {
controlStream := NewControlStream(
obs,
mockConnectedFuse{},
&NamedTunnelProperties{},
&TunnelProperties{},
1,
nil,
rpcClientFactory.newMockRPCClient,
1*time.Second,
nil,
1*time.Second,
HTTP2,
@@ -413,10 +415,11 @@ func TestFailRegistration(t *testing.T) {
controlStream := NewControlStream(
obs,
mockConnectedFuse{},
&NamedTunnelProperties{},
&TunnelProperties{},
http2Conn.connIndex,
nil,
rpcClientFactory.newMockRPCClient,
1*time.Second,
nil,
1*time.Second,
HTTP2,
@@ -461,10 +464,11 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
controlStream := NewControlStream(
obs,
mockConnectedFuse{},
&NamedTunnelProperties{},
&TunnelProperties{},
http2Conn.connIndex,
nil,
rpcClientFactory.newMockRPCClient,
1*time.Second,
shutdownC,
1*time.Second,
HTTP2,
@@ -546,7 +550,7 @@ func benchmarkServeHTTP(b *testing.B, test testRequest) {
require.NoError(b, err)
require.Equal(b, test.expectedStatus, resp.StatusCode)
if test.expectedBody != nil {
respBody, err := ioutil.ReadAll(resp.Body)
respBody, err := io.ReadAll(resp.Body)
require.NoError(b, err)
require.Equal(b, test.expectedBody, respBody)
}
-287
View File
@@ -2,11 +2,8 @@ package connection
import (
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/cloudflare/cloudflared/h2mux"
)
const (
@@ -16,34 +13,12 @@ const (
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 {
pushes prometheus.Counter
pushesErrors prometheus.Counter
}
type tunnelMetrics struct {
timerRetries prometheus.Gauge
serverLocations *prometheus.GaugeVec
// locationLock is a mutex for oldServerLocations
locationLock sync.Mutex
@@ -54,7 +29,6 @@ type tunnelMetrics struct {
regFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA
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
func initTunnelMetrics() *tunnelMetrics {
maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec(
@@ -351,15 +79,6 @@ func initTunnelMetrics() *tunnelMetrics {
)
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(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
@@ -416,10 +135,8 @@ func initTunnelMetrics() *tunnelMetrics {
prometheus.MustRegister(registerSuccess)
return &tunnelMetrics{
timerRetries: timerRetries,
serverLocations: serverLocations,
oldServerLocations: make(map[string]string),
muxerMetrics: newMuxerMetrics(),
tunnelsHA: newTunnelsForHA(),
regSuccess: registerSuccess,
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) {
t.locationLock.Lock()
defer t.locationLock.Unlock()
+2 -3
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) {
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
o.log.Info().
Int(management.EventTypeKey, int(management.Cloudflared)).
Str(LogFieldConnectionID, connectionID.String()).
@@ -63,8 +62,8 @@ func (o *Observer) sendRegisteringEvent(connIndex uint8) {
o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel})
}
func (o *Observer) sendConnectedEvent(connIndex uint8, protocol Protocol, location string) {
o.sendEvent(Event{Index: connIndex, EventType: Connected, Protocol: protocol, Location: location})
func (o *Observer) sendConnectedEvent(connIndex uint8, protocol Protocol, location string, edgeAddress net.IP) {
o.sendEvent(Event{Index: connIndex, EventType: Connected, Protocol: protocol, Location: location, EdgeAddress: edgeAddress})
}
func (o *Observer) SendURL(url string) {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
const (
AvailableProtocolFlagMessage = "Available protocols: 'auto' - automatically chooses the best protocol over time (the default; and also the recommended one); 'quic' - based on QUIC, relying on UDP egress to Cloudflare edge; 'http2' - using Go's HTTP2 library, relying on TCP egress to Cloudflare edge"
// edgeH2muxTLSServerName is the server name to establish h2mux connection with edge
// edgeH2muxTLSServerName is the server name to establish h2mux connection with edge (unused, but kept for legacy reference).
edgeH2muxTLSServerName = "cftunnel.com"
// edgeH2TLSServerName is the server name to establish http2 connection with edge
edgeH2TLSServerName = "h2.cftunnel.com"
+24 -564
View File
@@ -1,48 +1,16 @@
package connection
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"strconv"
"strings"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/datagramsession"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/management"
"github.com/cloudflare/cloudflared/packet"
quicpogs "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tracing"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
const (
// HTTPHeaderKey is used to get or set http headers in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPHeaderKey = "HttpHeader"
// HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPMethodKey = "HttpMethod"
// HTTPHostKey is used to get or set http Method in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPHostKey = "HttpHost"
QUICMetadataFlowID = "FlowID"
// emperically this capacity has been working well
demuxChanCapacity = 16
)
var (
@@ -50,40 +18,21 @@ var (
portMapMutex sync.Mutex
)
// QUICConnection represents the type that facilitates Proxying via QUIC streams.
type QUICConnection struct {
session quic.Connection
logger *zerolog.Logger
orchestrator Orchestrator
// sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer
sessionManager datagramsession.Manager
// datagramMuxer mux/demux datagrams from quic connection
datagramMuxer *quicpogs.DatagramMuxerV2
packetRouter *ingress.PacketRouter
controlStreamHandler ControlStreamHandler
connOptions *tunnelpogs.ConnectionOptions
connIndex uint8
}
// NewQUICConnection returns a new instance of QUICConnection.
func NewQUICConnection(
func DialQuic(
ctx context.Context,
quicConfig *quic.Config,
edgeAddr net.Addr,
tlsConfig *tls.Config,
edgeAddr netip.AddrPort,
localAddr net.IP,
connIndex uint8,
tlsConfig *tls.Config,
orchestrator Orchestrator,
connOptions *tunnelpogs.ConnectionOptions,
controlStreamHandler ControlStreamHandler,
logger *zerolog.Logger,
packetRouterConfig *ingress.GlobalRouterConfig,
) (*QUICConnection, error) {
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, logger)
) (quic.Connection, error) {
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, logger)
if err != nil {
return nil, err
}
session, err := quic.Dial(udpConn, edgeAddr, edgeAddr.String(), tlsConfig, quicConfig)
conn, err := quic.Dial(ctx, udpConn, net.UDPAddrFromAddrPort(edgeAddr), tlsConfig, quicConfig)
if err != nil {
// close the udp server socket in case of error connecting to the edge
udpConn.Close()
@@ -91,520 +40,31 @@ func NewQUICConnection(
}
// wrap the session, so that the UDPConn is closed after session is closed.
session = &wrapCloseableConnQuicConnection{
session,
conn = &wrapCloseableConnQuicConnection{
conn,
udpConn,
}
sessionDemuxChan := make(chan *packet.Session, demuxChanCapacity)
datagramMuxer := quicpogs.NewDatagramMuxerV2(session, logger, sessionDemuxChan)
sessionManager := datagramsession.NewManager(logger, datagramMuxer.SendToSession, sessionDemuxChan)
packetRouter := ingress.NewPacketRouter(packetRouterConfig, datagramMuxer, logger, orchestrator.WarpRoutingEnabled)
return &QUICConnection{
session: session,
orchestrator: orchestrator,
logger: logger,
sessionManager: sessionManager,
datagramMuxer: datagramMuxer,
packetRouter: packetRouter,
controlStreamHandler: controlStreamHandler,
connOptions: connOptions,
connIndex: connIndex,
}, nil
return conn, nil
}
// Serve starts a QUIC session that begins accepting streams.
func (q *QUICConnection) Serve(ctx context.Context) error {
// origintunneld assumes the first stream is used for the control plane
controlStream, err := q.session.OpenStream()
if err != nil {
return fmt.Errorf("failed to open a registration control stream: %w", err)
}
// If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits
// as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this
// connection).
// If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the
// other goroutine as fast as possible.
ctx, cancel := context.WithCancel(ctx)
errGroup, ctx := errgroup.WithContext(ctx)
// In the future, if cloudflared can autonomously push traffic to the edge, we have to make sure the control
// stream is already fully registered before the other goroutines can proceed.
errGroup.Go(func() error {
defer cancel()
return q.serveControlStream(ctx, controlStream)
})
errGroup.Go(func() error {
defer cancel()
return q.acceptStream(ctx)
})
errGroup.Go(func() error {
defer cancel()
return q.sessionManager.Serve(ctx)
})
errGroup.Go(func() error {
defer cancel()
return q.datagramMuxer.ServeReceive(ctx)
})
errGroup.Go(func() error {
defer cancel()
return q.packetRouter.Serve(ctx)
})
return errGroup.Wait()
}
func (q *QUICConnection) serveControlStream(ctx context.Context, controlStream quic.Stream) error {
// This blocks until the control plane is done.
err := q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions, q.orchestrator)
if err != nil {
// Not wrapping error here to be consistent with the http2 message.
return err
}
return nil
}
// Close closes the session with no errors specified.
func (q *QUICConnection) Close() {
q.session.CloseWithError(0, "")
}
func (q *QUICConnection) acceptStream(ctx context.Context) error {
defer q.Close()
for {
quicStream, err := q.session.AcceptStream(ctx)
if err != nil {
// context.Canceled is usually a user ctrl+c. We don't want to log an error here as it's intentional.
if errors.Is(err, context.Canceled) || q.controlStreamHandler.IsStopped() {
return nil
}
return fmt.Errorf("failed to accept QUIC stream: %w", err)
}
go q.runStream(quicStream)
}
}
func (q *QUICConnection) runStream(quicStream quic.Stream) {
ctx := quicStream.Context()
stream := quicpogs.NewSafeStreamCloser(quicStream)
defer stream.Close()
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
// code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream.
// So, we wrap the stream with a no-op write closer and only this method can actually close write side of the stream.
// A call to close will simulate a close to the read-side, which will fail subsequent reads.
noCloseStream := &nopCloserReadWriter{ReadWriteCloser: stream}
if err := q.handleStream(ctx, noCloseStream); err != nil {
q.logger.Debug().Err(err).Msg("Failed to handle QUIC stream")
// if we received an error at this level, then close write side of stream with an error, which will result in
// RST_STREAM frame.
quicStream.CancelWrite(0)
}
}
func (q *QUICConnection) handleStream(ctx context.Context, stream io.ReadWriteCloser) error {
signature, err := quicpogs.DetermineProtocol(stream)
if err != nil {
return err
}
switch signature {
case quicpogs.DataStreamProtocolSignature:
reqServerStream, err := quicpogs.NewRequestServerStream(stream, signature)
if err != nil {
return err
}
return q.handleDataStream(ctx, reqServerStream)
case quicpogs.RPCStreamProtocolSignature:
rpcStream, err := quicpogs.NewRPCServerStream(stream, signature)
if err != nil {
return err
}
return q.handleRPCStream(rpcStream)
default:
return fmt.Errorf("unknown protocol %v", signature)
}
}
func (q *QUICConnection) handleDataStream(ctx context.Context, stream *quicpogs.RequestServerStream) error {
request, err := stream.ReadConnectRequestData()
if err != nil {
return err
}
if err, connectResponseSent := q.dispatchRequest(ctx, stream, err, request); err != nil {
q.logger.Err(err).Str("type", request.Type.String()).Str("dest", request.Dest).Msg("Request failed")
// if the connectResponse was already sent and we had an error, we need to propagate it up, so that the stream is
// closed with an RST_STREAM frame
if connectResponseSent {
return err
}
if writeRespErr := stream.WriteConnectResponseData(err); writeRespErr != nil {
return writeRespErr
}
}
return nil
}
// dispatchRequest will dispatch the request depending on the type and returns an error if it occurs.
// More importantly, it also tells if the during processing of the request the ConnectResponse metadata was sent downstream.
// This is important since it informs
func (q *QUICConnection) dispatchRequest(ctx context.Context, stream *quicpogs.RequestServerStream, err error, request *quicpogs.ConnectRequest) (error, bool) {
originProxy, err := q.orchestrator.GetOriginProxy()
if err != nil {
return err, false
}
switch request.Type {
case quicpogs.ConnectionTypeHTTP, quicpogs.ConnectionTypeWebsocket:
tracedReq, err := buildHTTPRequest(ctx, request, stream, q.connIndex, q.logger)
if err != nil {
return err, false
}
w := newHTTPResponseAdapter(stream)
return originProxy.ProxyHTTP(&w, tracedReq, request.Type == quicpogs.ConnectionTypeWebsocket), w.connectResponseSent
case quicpogs.ConnectionTypeTCP:
rwa := &streamReadWriteAcker{RequestServerStream: stream}
metadata := request.MetadataMap()
return originProxy.ProxyTCP(ctx, rwa, &TCPRequest{
Dest: request.Dest,
FlowID: metadata[QUICMetadataFlowID],
CfTraceID: metadata[tracing.TracerContextName],
ConnIndex: q.connIndex,
}), rwa.connectResponseSent
default:
return errors.Errorf("unsupported error type: %s", request.Type), false
}
}
func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) error {
if err := rpcStream.Serve(q, q, q.logger); err != nil {
q.logger.Err(err).Msg("failed handling RPC stream")
}
return nil
}
// RegisterUdpSession is the RPC method invoked by edge to register and run a session
func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*tunnelpogs.RegisterUdpSessionResponse, error) {
traceCtx := tracing.NewTracedContext(ctx, traceContext, q.logger)
ctx, registerSpan := traceCtx.Tracer().Start(traceCtx, "register-session", trace.WithAttributes(
attribute.String("session-id", sessionID.String()),
attribute.String("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)),
))
log := q.logger.With().Int(management.EventTypeKey, int(management.UDP)).Logger()
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
originProxy, err := ingress.DialUDP(dstIP, dstPort)
if err != nil {
log.Err(err).Msgf("Failed to create udp proxy to %s:%d", dstIP, dstPort)
tracing.EndWithErrorStatus(registerSpan, err)
return nil, err
}
registerSpan.SetAttributes(
attribute.Bool("socket-bind-success", true),
attribute.String("src", originProxy.LocalAddr().String()),
)
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
if err != nil {
log.Err(err).Str("sessionID", sessionID.String()).Msgf("Failed to register udp session")
tracing.EndWithErrorStatus(registerSpan, err)
return nil, err
}
go q.serveUDPSession(session, closeAfterIdleHint)
log.Debug().
Str("sessionID", sessionID.String()).
Str("src", originProxy.LocalAddr().String()).
Str("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)).
Msgf("Registered session")
tracing.End(registerSpan)
resp := tunnelpogs.RegisterUdpSessionResponse{
Spans: traceCtx.GetProtoSpans(),
}
return &resp, nil
}
func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, closeAfterIdleHint time.Duration) {
ctx := q.session.Context()
closedByRemote, err := session.Serve(ctx, closeAfterIdleHint)
// If session is terminated by remote, then we know it has been unregistered from session manager and edge
if !closedByRemote {
if err != nil {
q.closeUDPSession(ctx, session.ID, err.Error())
} else {
q.closeUDPSession(ctx, session.ID, "terminated without error")
}
}
q.logger.Debug().Err(err).
Int(management.EventTypeKey, int(management.UDP)).
Str("sessionID", session.ID.String()).
Msg("Session terminated")
}
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) {
q.sessionManager.UnregisterSession(ctx, sessionID, message, false)
stream, err := q.session.OpenStream()
if err != nil {
// Log this at debug because this is not an error if session was closed due to lost connection
// with edge
q.logger.Debug().Err(err).
Int(management.EventTypeKey, int(management.UDP)).
Str("sessionID", sessionID.String()).
Msgf("Failed to open quic stream to unregister udp session with edge")
return
}
rpcClientStream, err := quicpogs.NewRPCClientStream(ctx, stream, q.logger)
if err != nil {
// Log this at debug because this is not an error if session was closed due to lost connection
// with edge
q.logger.Err(err).Str("sessionID", sessionID.String()).
Msgf("Failed to open rpc stream to unregister udp session with edge")
return
}
if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil {
q.logger.Err(err).Str("sessionID", sessionID.String()).
Msgf("Failed to unregister udp session with edge")
}
}
// UnregisterUdpSession is the RPC method invoked by edge to unregister and terminate a sesssion
func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
return q.sessionManager.UnregisterSession(ctx, sessionID, message, true)
}
// UpdateConfiguration is the RPC method invoked by edge when there is a new configuration
func (q *QUICConnection) UpdateConfiguration(ctx context.Context, version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
return q.orchestrator.UpdateConfig(version, config)
}
// streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to
// the client.
type streamReadWriteAcker struct {
*quicpogs.RequestServerStream
connectResponseSent bool
}
// AckConnection acks response back to the proxy.
func (s *streamReadWriteAcker) AckConnection(tracePropagation string) error {
metadata := []quicpogs.Metadata{}
// Only add tracing if provided by origintunneld
if tracePropagation != "" {
metadata = append(metadata, quicpogs.Metadata{
Key: tracing.CanonicalCloudflaredTracingHeader,
Val: tracePropagation,
})
}
s.connectResponseSent = true
return s.WriteConnectResponseData(nil, metadata...)
}
// httpResponseAdapter translates responses written by the HTTP Proxy into ones that can be used in QUIC.
type httpResponseAdapter struct {
*quicpogs.RequestServerStream
headers http.Header
connectResponseSent bool
}
func newHTTPResponseAdapter(s *quicpogs.RequestServerStream) httpResponseAdapter {
return httpResponseAdapter{RequestServerStream: s, headers: make(http.Header)}
}
func (hrw *httpResponseAdapter) AddTrailer(trailerName, trailerValue string) {
// we do not support trailers over QUIC
}
func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
metadata := make([]quicpogs.Metadata, 0)
metadata = append(metadata, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(status)})
for k, vv := range header {
for _, v := range vv {
httpHeaderKey := fmt.Sprintf("%s:%s", HTTPHeaderKey, k)
metadata = append(metadata, quicpogs.Metadata{Key: httpHeaderKey, Val: v})
}
}
return hrw.WriteConnectResponseData(nil, metadata...)
}
func (hrw *httpResponseAdapter) Header() http.Header {
return hrw.headers
}
func (hrw *httpResponseAdapter) WriteHeader(status int) {
hrw.WriteRespHeaders(status, hrw.headers)
}
func (hrw *httpResponseAdapter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
conn := &localProxyConnection{hrw.ReadWriteCloser}
readWriter := bufio.NewReadWriter(
bufio.NewReader(hrw.ReadWriteCloser),
bufio.NewWriter(hrw.ReadWriteCloser),
)
return conn, readWriter, nil
}
func (hrw *httpResponseAdapter) WriteErrorResponse(err error) {
hrw.WriteConnectResponseData(err, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
}
func (hrw *httpResponseAdapter) WriteConnectResponseData(respErr error, metadata ...quicpogs.Metadata) error {
hrw.connectResponseSent = true
return hrw.RequestServerStream.WriteConnectResponseData(respErr, metadata...)
}
func buildHTTPRequest(
ctx context.Context,
connectRequest *quicpogs.ConnectRequest,
body io.ReadCloser,
connIndex uint8,
log *zerolog.Logger,
) (*tracing.TracedHTTPRequest, error) {
metadata := connectRequest.MetadataMap()
dest := connectRequest.Dest
method := metadata[HTTPMethodKey]
host := metadata[HTTPHostKey]
isWebsocket := connectRequest.Type == quicpogs.ConnectionTypeWebsocket
req, err := http.NewRequestWithContext(ctx, method, dest, body)
if err != nil {
return nil, err
}
req.Host = host
for _, metadata := range connectRequest.Metadata {
if strings.Contains(metadata.Key, HTTPHeaderKey) {
// metadata.Key is off the format httpHeaderKey:<HTTPHeader>
httpHeaderKey := strings.Split(metadata.Key, ":")
if len(httpHeaderKey) != 2 {
return nil, fmt.Errorf("header Key: %s malformed", metadata.Key)
}
req.Header.Add(httpHeaderKey[1], metadata.Val)
}
}
// Go's http.Client automatically sends chunked request body if this value is not set on the
// *http.Request struct regardless of header:
// https://go.googlesource.com/go/+/go1.8rc2/src/net/http/transfer.go#154.
if err := setContentLength(req); err != nil {
return nil, fmt.Errorf("Error setting content-length: %w", err)
}
// Go's client defaults to chunked encoding after a 200ms delay if the following cases are true:
// * the request body blocks
// * the content length is not set (or set to -1)
// * the method doesn't usually have a body (GET, HEAD, DELETE, ...)
// * there is no transfer-encoding=chunked already set.
// So, if transfer cannot be chunked and content length is 0, we dont set a request body.
if !isWebsocket && !isTransferEncodingChunked(req) && req.ContentLength == 0 {
req.Body = http.NoBody
}
stripWebsocketUpgradeHeader(req)
// Check for tracing on request
tracedReq := tracing.NewTracedHTTPRequest(req, connIndex, log)
return tracedReq, err
}
func setContentLength(req *http.Request) error {
var err error
if contentLengthStr := req.Header.Get("Content-Length"); contentLengthStr != "" {
req.ContentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
}
return err
}
func isTransferEncodingChunked(req *http.Request) bool {
transferEncodingVal := req.Header.Get("Transfer-Encoding")
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding suggests that this can be a comma
// separated value as well.
return strings.Contains(strings.ToLower(transferEncodingVal), "chunked")
}
// A helper struct that guarantees a call to close only affects read side, but not write side.
type nopCloserReadWriter struct {
io.ReadWriteCloser
// for use by Read only
// we don't need a memory barrier here because there is an implicit assumption that
// Read calls can't happen concurrently by different go-routines.
sawEOF bool
// should be updated and read using atomic primitives.
// value is read in Read method and written in Close method, which could be done by different
// go-routines.
closed uint32
}
func (np *nopCloserReadWriter) Read(p []byte) (n int, err error) {
if np.sawEOF {
return 0, io.EOF
}
if atomic.LoadUint32(&np.closed) > 0 {
return 0, fmt.Errorf("closed by handler")
}
n, err = np.ReadWriteCloser.Read(p)
if err == io.EOF {
np.sawEOF = true
}
return
}
func (np *nopCloserReadWriter) Close() error {
atomic.StoreUint32(&np.closed, 1)
return nil
}
// muxerWrapper wraps DatagramMuxerV2 to satisfy the packet.FunnelUniPipe interface
type muxerWrapper struct {
muxer *quicpogs.DatagramMuxerV2
}
func (rp *muxerWrapper) SendPacket(dst netip.Addr, pk packet.RawPacket) error {
return rp.muxer.SendPacket(quicpogs.RawPacket(pk))
}
func (rp *muxerWrapper) ReceivePacket(ctx context.Context) (packet.RawPacket, error) {
pk, err := rp.muxer.ReceivePacket(ctx)
if err != nil {
return packet.RawPacket{}, err
}
rawPacket, ok := pk.(quicpogs.RawPacket)
if ok {
return packet.RawPacket(rawPacket), nil
}
return packet.RawPacket{}, fmt.Errorf("unexpected packet type %+v", pk)
}
func (rp *muxerWrapper) Close() error {
return nil
}
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, logger *zerolog.Logger) (*net.UDPConn, error) {
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, logger *zerolog.Logger) (*net.UDPConn, error) {
portMapMutex.Lock()
defer portMapMutex.Unlock()
if localIP == nil {
localIP = net.IPv4zero
listenNetwork := "udp"
// https://github.com/quic-go/quic-go/issues/3793 DF bit cannot be set for dual stack listener ("udp") on macOS,
// to set the DF bit properly, the network string needs to be specific to the IP family.
if runtime.GOOS == "darwin" {
if edgeIP.Addr().Is4() {
listenNetwork = "udp4"
} else {
listenNetwork = "udp6"
}
}
// if port was not set yet, it will be zero, so bind will randomly allocate one.
if port, ok := portForConnIndex[connIndex]; ok {
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP, Port: port})
udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: port})
// if there wasn't an error, or if port was 0 (independently of error or not, just return)
if err == nil {
return udpConn, nil
@@ -614,7 +74,7 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, logger *zerolog.
}
// if we reached here, then there was an error or port as not been allocated it.
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: localIP, Port: 0})
udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0})
if err == nil {
udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr)
if !ok {
+417
View File
@@ -0,0 +1,417 @@
package connection
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"golang.org/x/sync/errgroup"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tracing"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
)
const (
// HTTPHeaderKey is used to get or set http headers in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPHeaderKey = "HttpHeader"
// HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPMethodKey = "HttpMethod"
// HTTPHostKey is used to get or set http host in QUIC ALPN if the underlying proxy connection type is HTTP.
HTTPHostKey = "HttpHost"
QUICMetadataFlowID = "FlowID"
)
// quicConnection represents the type that facilitates Proxying via QUIC streams.
type quicConnection struct {
conn quic.Connection
logger *zerolog.Logger
orchestrator Orchestrator
datagramHandler DatagramSessionHandler
controlStreamHandler ControlStreamHandler
connOptions *tunnelpogs.ConnectionOptions
connIndex uint8
rpcTimeout time.Duration
streamWriteTimeout time.Duration
gracePeriod time.Duration
}
// NewTunnelConnection takes a [quic.Connection] to wrap it for use with cloudflared application logic.
func NewTunnelConnection(
ctx context.Context,
conn quic.Connection,
connIndex uint8,
orchestrator Orchestrator,
datagramSessionHandler DatagramSessionHandler,
controlStreamHandler ControlStreamHandler,
connOptions *pogs.ConnectionOptions,
rpcTimeout time.Duration,
streamWriteTimeout time.Duration,
gracePeriod time.Duration,
logger *zerolog.Logger,
) (TunnelConnection, error) {
return &quicConnection{
conn: conn,
logger: logger,
orchestrator: orchestrator,
datagramHandler: datagramSessionHandler,
controlStreamHandler: controlStreamHandler,
connOptions: connOptions,
connIndex: connIndex,
rpcTimeout: rpcTimeout,
streamWriteTimeout: streamWriteTimeout,
gracePeriod: gracePeriod,
}, nil
}
// Serve starts a QUIC connection that begins accepting streams.
func (q *quicConnection) Serve(ctx context.Context) error {
// The edge assumes the first stream is used for the control plane
controlStream, err := q.conn.OpenStream()
if err != nil {
return fmt.Errorf("failed to open a registration control stream: %w", err)
}
// If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits
// as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this
// connection).
// If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the
// other goroutine as fast as possible.
ctx, cancel := context.WithCancel(ctx)
errGroup, ctx := errgroup.WithContext(ctx)
// In the future, if cloudflared can autonomously push traffic to the edge, we have to make sure the control
// stream is already fully registered before the other goroutines can proceed.
errGroup.Go(func() error {
// err is equal to nil if we exit due to unregistration. If that happens we want to wait the full
// amount of the grace period, allowing requests to finish before we cancel the context, which will
// make cloudflared exit.
if err := q.serveControlStream(ctx, controlStream); err == nil {
select {
case <-ctx.Done():
case <-time.Tick(q.gracePeriod):
}
}
cancel()
return err
})
errGroup.Go(func() error {
defer cancel()
return q.acceptStream(ctx)
})
errGroup.Go(func() error {
defer cancel()
return q.datagramHandler.Serve(ctx)
})
return errGroup.Wait()
}
// serveControlStream will serve the RPC; blocking until the control plane is done.
func (q *quicConnection) serveControlStream(ctx context.Context, controlStream quic.Stream) error {
return q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions, q.orchestrator)
}
// Close the connection with no errors specified.
func (q *quicConnection) Close() {
q.conn.CloseWithError(0, "")
}
func (q *quicConnection) acceptStream(ctx context.Context) error {
defer q.Close()
for {
quicStream, err := q.conn.AcceptStream(ctx)
if err != nil {
// context.Canceled is usually a user ctrl+c. We don't want to log an error here as it's intentional.
if errors.Is(err, context.Canceled) || q.controlStreamHandler.IsStopped() {
return nil
}
return fmt.Errorf("failed to accept QUIC stream: %w", err)
}
go q.runStream(quicStream)
}
}
func (q *quicConnection) runStream(quicStream quic.Stream) {
ctx := quicStream.Context()
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
defer stream.Close()
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
// code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream.
// So, we wrap the stream with a no-op write closer and only this method can actually close write side of the stream.
// A call to close will simulate a close to the read-side, which will fail subsequent reads.
noCloseStream := &nopCloserReadWriter{ReadWriteCloser: stream}
ss := rpcquic.NewCloudflaredServer(q.handleDataStream, q.datagramHandler, q, q.rpcTimeout)
if err := ss.Serve(ctx, noCloseStream); err != nil {
q.logger.Debug().Err(err).Msg("Failed to handle QUIC stream")
// if we received an error at this level, then close write side of stream with an error, which will result in
// RST_STREAM frame.
quicStream.CancelWrite(0)
}
}
func (q *quicConnection) handleDataStream(ctx context.Context, stream *rpcquic.RequestServerStream) error {
request, err := stream.ReadConnectRequestData()
if err != nil {
return err
}
if err, connectResponseSent := q.dispatchRequest(ctx, stream, request); err != nil {
q.logger.Err(err).Str("type", request.Type.String()).Str("dest", request.Dest).Msg("Request failed")
// if the connectResponse was already sent and we had an error, we need to propagate it up, so that the stream is
// closed with an RST_STREAM frame
if connectResponseSent {
return err
}
if writeRespErr := stream.WriteConnectResponseData(err); writeRespErr != nil {
return writeRespErr
}
}
return nil
}
// dispatchRequest will dispatch the request to the origin depending on the type and returns an error if it occurs.
// Also returns if the connect response was sent to the downstream during processing of the origin request.
func (q *quicConnection) dispatchRequest(ctx context.Context, stream *rpcquic.RequestServerStream, request *pogs.ConnectRequest) (err error, connectResponseSent bool) {
originProxy, err := q.orchestrator.GetOriginProxy()
if err != nil {
return err, false
}
switch request.Type {
case pogs.ConnectionTypeHTTP, pogs.ConnectionTypeWebsocket:
tracedReq, err := buildHTTPRequest(ctx, request, stream, q.connIndex, q.logger)
if err != nil {
return err, false
}
w := newHTTPResponseAdapter(stream)
return originProxy.ProxyHTTP(&w, tracedReq, request.Type == pogs.ConnectionTypeWebsocket), w.connectResponseSent
case pogs.ConnectionTypeTCP:
rwa := &streamReadWriteAcker{RequestServerStream: stream}
metadata := request.MetadataMap()
return originProxy.ProxyTCP(ctx, rwa, &TCPRequest{
Dest: request.Dest,
FlowID: metadata[QUICMetadataFlowID],
CfTraceID: metadata[tracing.TracerContextName],
ConnIndex: q.connIndex,
}), rwa.connectResponseSent
default:
return errors.Errorf("unsupported error type: %s", request.Type), false
}
}
// UpdateConfiguration is the RPC method invoked by edge when there is a new configuration
func (q *quicConnection) UpdateConfiguration(ctx context.Context, version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
return q.orchestrator.UpdateConfig(version, config)
}
// streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to
// the client.
type streamReadWriteAcker struct {
*rpcquic.RequestServerStream
connectResponseSent bool
}
// AckConnection acks response back to the proxy.
func (s *streamReadWriteAcker) AckConnection(tracePropagation string) error {
metadata := []pogs.Metadata{}
// Only add tracing if provided by the edge request
if tracePropagation != "" {
metadata = append(metadata, pogs.Metadata{
Key: tracing.CanonicalCloudflaredTracingHeader,
Val: tracePropagation,
})
}
s.connectResponseSent = true
return s.WriteConnectResponseData(nil, metadata...)
}
// httpResponseAdapter translates responses written by the HTTP Proxy into ones that can be used in QUIC.
type httpResponseAdapter struct {
*rpcquic.RequestServerStream
headers http.Header
connectResponseSent bool
}
func newHTTPResponseAdapter(s *rpcquic.RequestServerStream) httpResponseAdapter {
return httpResponseAdapter{RequestServerStream: s, headers: make(http.Header)}
}
func (hrw *httpResponseAdapter) AddTrailer(trailerName, trailerValue string) {
// we do not support trailers over QUIC
}
func (hrw *httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
metadata := make([]pogs.Metadata, 0)
metadata = append(metadata, pogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(status)})
for k, vv := range header {
for _, v := range vv {
httpHeaderKey := fmt.Sprintf("%s:%s", HTTPHeaderKey, k)
metadata = append(metadata, pogs.Metadata{Key: httpHeaderKey, Val: v})
}
}
return hrw.WriteConnectResponseData(nil, metadata...)
}
func (hrw *httpResponseAdapter) Write(p []byte) (int, error) {
// Make sure to send WriteHeader response if not called yet
if !hrw.connectResponseSent {
hrw.WriteRespHeaders(http.StatusOK, hrw.headers)
}
return hrw.RequestServerStream.Write(p)
}
func (hrw *httpResponseAdapter) Header() http.Header {
return hrw.headers
}
// This is a no-op Flush because this adapter is over a quic.Stream and we don't need Flush here.
func (hrw *httpResponseAdapter) Flush() {}
func (hrw *httpResponseAdapter) WriteHeader(status int) {
hrw.WriteRespHeaders(status, hrw.headers)
}
func (hrw *httpResponseAdapter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
conn := &localProxyConnection{hrw.ReadWriteCloser}
readWriter := bufio.NewReadWriter(
bufio.NewReader(hrw.ReadWriteCloser),
bufio.NewWriter(hrw.ReadWriteCloser),
)
return conn, readWriter, nil
}
func (hrw *httpResponseAdapter) WriteErrorResponse(err error) {
hrw.WriteConnectResponseData(err, pogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
}
func (hrw *httpResponseAdapter) WriteConnectResponseData(respErr error, metadata ...pogs.Metadata) error {
hrw.connectResponseSent = true
return hrw.RequestServerStream.WriteConnectResponseData(respErr, metadata...)
}
func buildHTTPRequest(
ctx context.Context,
connectRequest *pogs.ConnectRequest,
body io.ReadCloser,
connIndex uint8,
log *zerolog.Logger,
) (*tracing.TracedHTTPRequest, error) {
metadata := connectRequest.MetadataMap()
dest := connectRequest.Dest
method := metadata[HTTPMethodKey]
host := metadata[HTTPHostKey]
isWebsocket := connectRequest.Type == pogs.ConnectionTypeWebsocket
req, err := http.NewRequestWithContext(ctx, method, dest, body)
if err != nil {
return nil, err
}
req.Host = host
for _, metadata := range connectRequest.Metadata {
if strings.Contains(metadata.Key, HTTPHeaderKey) {
// metadata.Key is off the format httpHeaderKey:<HTTPHeader>
httpHeaderKey := strings.Split(metadata.Key, ":")
if len(httpHeaderKey) != 2 {
return nil, fmt.Errorf("header Key: %s malformed", metadata.Key)
}
req.Header.Add(httpHeaderKey[1], metadata.Val)
}
}
// Go's http.Client automatically sends chunked request body if this value is not set on the
// *http.Request struct regardless of header:
// https://go.googlesource.com/go/+/go1.8rc2/src/net/http/transfer.go#154.
if err := setContentLength(req); err != nil {
return nil, fmt.Errorf("Error setting content-length: %w", err)
}
// Go's client defaults to chunked encoding after a 200ms delay if the following cases are true:
// * the request body blocks
// * the content length is not set (or set to -1)
// * the method doesn't usually have a body (GET, HEAD, DELETE, ...)
// * there is no transfer-encoding=chunked already set.
// So, if transfer cannot be chunked and content length is 0, we dont set a request body.
if !isWebsocket && !isTransferEncodingChunked(req) && req.ContentLength == 0 {
req.Body = http.NoBody
}
stripWebsocketUpgradeHeader(req)
// Check for tracing on request
tracedReq := tracing.NewTracedHTTPRequest(req, connIndex, log)
return tracedReq, err
}
func setContentLength(req *http.Request) error {
var err error
if contentLengthStr := req.Header.Get("Content-Length"); contentLengthStr != "" {
req.ContentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
}
return err
}
func isTransferEncodingChunked(req *http.Request) bool {
transferEncodingVal := req.Header.Get("Transfer-Encoding")
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding suggests that this can be a comma
// separated value as well.
return strings.Contains(strings.ToLower(transferEncodingVal), "chunked")
}
// A helper struct that guarantees a call to close only affects read side, but not write side.
type nopCloserReadWriter struct {
io.ReadWriteCloser
// for use by Read only
// we don't need a memory barrier here because there is an implicit assumption that
// Read calls can't happen concurrently by different go-routines.
sawEOF bool
// should be updated and read using atomic primitives.
// value is read in Read method and written in Close method, which could be done by different
// go-routines.
closed uint32
}
func (np *nopCloserReadWriter) Read(p []byte) (n int, err error) {
if np.sawEOF {
return 0, io.EOF
}
if atomic.LoadUint32(&np.closed) > 0 {
return 0, fmt.Errorf("closed by handler")
}
n, err = np.ReadWriteCloser.Read(p)
if err == io.EOF {
np.sawEOF = true
}
return
}
func (np *nopCloserReadWriter) Close() error {
atomic.StoreUint32(&np.closed, 1)
return nil
}
@@ -3,39 +3,47 @@ package connection
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"math/big"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/gobwas/ws/wsutil"
"github.com/google/uuid"
"github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/nettest"
"github.com/cloudflare/cloudflared/datagramsession"
quicpogs "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/packet"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tracing"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
)
var (
testTLSServerConfig = quicpogs.GenerateTLSConfig()
testTLSServerConfig = GenerateTLSConfig()
testQUICConfig = &quic.Config{
ConnectionIDLength: 16,
KeepAlivePeriod: 5 * time.Second,
EnableDatagrams: true,
KeepAlivePeriod: 5 * time.Second,
EnableDatagrams: true,
}
defaultQUICTimeout = 30 * time.Second
)
var _ ReadWriteAcker = (*streamReadWriteAcker)(nil)
@@ -43,13 +51,6 @@ var _ ReadWriteAcker = (*streamReadWriteAcker)(nil)
// TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol.
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
func TestQUICServer(t *testing.T) {
// Start a UDP Listener for QUIC.
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
require.NoError(t, err)
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
require.NoError(t, err)
defer udpListener.Close()
// This is simply a sample websocket frame message.
wsBuf := &bytes.Buffer{}
wsutil.WriteClientBinary(wsBuf, []byte("Hello"))
@@ -57,16 +58,16 @@ func TestQUICServer(t *testing.T) {
var tests = []struct {
desc string
dest string
connectionType quicpogs.ConnectionType
metadata []quicpogs.Metadata
connectionType pogs.ConnectionType
metadata []pogs.Metadata
message []byte
expectedResponse []byte
}{
{
desc: "test http proxy",
dest: "/ok",
connectionType: quicpogs.ConnectionTypeHTTP,
metadata: []quicpogs.Metadata{
connectionType: pogs.ConnectionTypeHTTP,
metadata: []pogs.Metadata{
{
Key: "HttpHeader:Cf-Ray",
Val: "123123123",
@@ -85,8 +86,8 @@ func TestQUICServer(t *testing.T) {
{
desc: "test http body request streaming",
dest: "/slow_echo_body",
connectionType: quicpogs.ConnectionTypeHTTP,
metadata: []quicpogs.Metadata{
connectionType: pogs.ConnectionTypeHTTP,
metadata: []pogs.Metadata{
{
Key: "HttpHeader:Cf-Ray",
Val: "123123123",
@@ -110,8 +111,8 @@ func TestQUICServer(t *testing.T) {
{
desc: "test ws proxy",
dest: "/ws/echo",
connectionType: quicpogs.ConnectionTypeWebsocket,
metadata: []quicpogs.Metadata{
connectionType: pogs.ConnectionTypeWebsocket,
metadata: []pogs.Metadata{
{
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
Val: "Websocket",
@@ -134,8 +135,8 @@ func TestQUICServer(t *testing.T) {
},
{
desc: "test tcp proxy",
connectionType: quicpogs.ConnectionTypeTCP,
metadata: []quicpogs.Metadata{},
connectionType: pogs.ConnectionTypeTCP,
metadata: []pogs.Metadata{},
message: []byte("Here is some tcp data"),
expectedResponse: []byte("Here is some tcp data"),
},
@@ -145,8 +146,14 @@ func TestQUICServer(t *testing.T) {
test := test // capture range variable
t.Run(test.desc, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
quicListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig)
// Start a UDP Listener for QUIC.
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
require.NoError(t, err)
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
require.NoError(t, err)
defer udpListener.Close()
quicTransport := &quic.Transport{Conn: udpListener, ConnectionIDLength: 16}
quicListener, err := quicTransport.Listen(testTLSServerConfig, testQUICConfig)
require.NoError(t, err)
serverDone := make(chan struct{})
@@ -157,11 +164,11 @@ func TestQUICServer(t *testing.T) {
close(serverDone)
}()
qc := testQUICConnection(udpListener.LocalAddr(), t, uint8(i))
tunnelConn, _ := testTunnelConnection(t, netip.MustParseAddrPort(udpListener.LocalAddr().String()), uint8(i))
connDone := make(chan struct{})
go func() {
qc.Serve(ctx)
tunnelConn.Serve(ctx)
close(connDone)
}()
@@ -176,7 +183,7 @@ type fakeControlStream struct {
ControlStreamHandler
}
func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error {
func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *pogs.ConnectionOptions, tunnelConfigGetter TunnelConfigJSONGetter) error {
<-ctx.Done()
return nil
}
@@ -187,10 +194,10 @@ func (fakeControlStream) IsStopped() bool {
func quicServer(
ctx context.Context,
t *testing.T,
listener quic.Listener,
listener *quic.Listener,
dest string,
connectionType quicpogs.ConnectionType,
metadata []quicpogs.Metadata,
connectionType pogs.ConnectionType,
metadata []pogs.Metadata,
message []byte,
expectedResponse []byte,
) {
@@ -199,9 +206,9 @@ func quicServer(
quicStream, err := session.OpenStreamSync(context.Background())
require.NoError(t, err)
stream := quicpogs.NewSafeStreamCloser(quicStream)
stream := cfdquic.NewSafeStreamCloser(quicStream, defaultQUICTimeout, &log)
reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream}
reqClientStream := rpcquic.RequestClientStream{ReadWriteCloser: stream}
err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...)
require.NoError(t, err)
@@ -266,15 +273,15 @@ func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, tr *tracing.T
func TestBuildHTTPRequest(t *testing.T) {
var tests = []struct {
name string
connectRequest *quicpogs.ConnectRequest
connectRequest *pogs.ConnectRequest
body io.ReadCloser
req *http.Request
}{
{
name: "check if http.Request is built correctly with content length",
connectRequest: &quicpogs.ConnectRequest{
connectRequest: &pogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
Metadata: []pogs.Metadata{
{
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
Val: "Websocket",
@@ -318,9 +325,9 @@ func TestBuildHTTPRequest(t *testing.T) {
},
{
name: "if content length isn't part of request headers, then it's not set",
connectRequest: &quicpogs.ConnectRequest{
connectRequest: &pogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
Metadata: []pogs.Metadata{
{
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
Val: "Websocket",
@@ -359,9 +366,9 @@ func TestBuildHTTPRequest(t *testing.T) {
},
{
name: "if content length is 0, but transfer-encoding is chunked, body is not nil",
connectRequest: &quicpogs.ConnectRequest{
connectRequest: &pogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
Metadata: []pogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
@@ -401,9 +408,9 @@ func TestBuildHTTPRequest(t *testing.T) {
},
{
name: "if content length is 0, but transfer-encoding is gzip,chunked, body is not nil",
connectRequest: &quicpogs.ConnectRequest{
connectRequest: &pogs.ConnectRequest{
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
Metadata: []pogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
@@ -443,10 +450,10 @@ func TestBuildHTTPRequest(t *testing.T) {
},
{
name: "if content length is 0, and connect request is a websocket, body is not nil",
connectRequest: &quicpogs.ConnectRequest{
Type: quicpogs.ConnectionTypeWebsocket,
connectRequest: &pogs.ConnectRequest{
Type: pogs.ConnectionTypeWebsocket,
Dest: "http://test.com",
Metadata: []quicpogs.Metadata{
Metadata: []pogs.Metadata{
{
Key: "HttpHeader:Another-Header",
Val: "Misc",
@@ -508,7 +515,6 @@ func TestServeUDPSession(t *testing.T) {
defer udpListener.Close()
ctx, cancel := context.WithCancel(context.Background())
val := udpListener.LocalAddr()
// Establish QUIC connection with edge
edgeQUICSessionChan := make(chan quic.Connection)
@@ -522,13 +528,14 @@ func TestServeUDPSession(t *testing.T) {
}()
// Random index to avoid reusing port
qc := testQUICConnection(val, t, 28)
go qc.Serve(ctx)
tunnelConn, datagramConn := testTunnelConnection(t, netip.MustParseAddrPort(udpListener.LocalAddr().String()), 28)
go tunnelConn.Serve(ctx)
edgeQUICSession := <-edgeQUICSessionChan
serveSession(ctx, qc, edgeQUICSession, closedByOrigin, io.EOF.Error(), t)
serveSession(ctx, qc, edgeQUICSession, closedByTimeout, datagramsession.SessionIdleErr(time.Millisecond*50).Error(), t)
serveSession(ctx, qc, edgeQUICSession, closedByRemote, "eyeball closed connection", t)
serveSession(ctx, datagramConn, edgeQUICSession, closedByOrigin, io.EOF.Error(), t)
serveSession(ctx, datagramConn, edgeQUICSession, closedByTimeout, datagramsession.SessionIdleErr(time.Millisecond*50).Error(), t)
serveSession(ctx, datagramConn, edgeQUICSession, closedByRemote, "eyeball closed connection", t)
cancel()
}
@@ -571,8 +578,20 @@ func TestNopCloserReadWriterCloseAfterEOF(t *testing.T) {
}
func TestCreateUDPConnReuseSourcePort(t *testing.T) {
edgeIPv4 := netip.MustParseAddrPort("0.0.0.0:0")
edgeIPv6 := netip.MustParseAddrPort("[::]:0")
// We assume the test environment has access to an IPv4 interface
testCreateUDPConnReuseSourcePortForEdgeIP(t, edgeIPv4)
if nettest.SupportsIPv6() {
testCreateUDPConnReuseSourcePortForEdgeIP(t, edgeIPv6)
}
}
func testCreateUDPConnReuseSourcePortForEdgeIP(t *testing.T, edgeIP netip.AddrPort) {
logger := zerolog.Nop()
conn, err := createUDPConnForConnIndex(0, nil, &logger)
conn, err := createUDPConnForConnIndex(0, nil, edgeIP, &logger)
require.NoError(t, err)
getPortFunc := func(conn *net.UDPConn) int {
@@ -586,44 +605,44 @@ func TestCreateUDPConnReuseSourcePort(t *testing.T) {
conn.Close()
// should get the same port as before.
conn, err = createUDPConnForConnIndex(0, nil, &logger)
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
require.NoError(t, err)
require.Equal(t, initialPort, getPortFunc(conn))
// new index, should get a different port
conn1, err := createUDPConnForConnIndex(1, nil, &logger)
conn1, err := createUDPConnForConnIndex(1, nil, edgeIP, &logger)
require.NoError(t, err)
require.NotEqual(t, initialPort, getPortFunc(conn1))
// not closing the conn and trying to obtain a new conn for same index should give a different random port
conn, err = createUDPConnForConnIndex(0, nil, &logger)
conn, err = createUDPConnForConnIndex(0, nil, edgeIP, &logger)
require.NoError(t, err)
require.NotEqual(t, initialPort, getPortFunc(conn))
}
func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) {
func serveSession(ctx context.Context, datagramConn *datagramV2Connection, edgeQUICSession quic.Connection, closeType closeReason, expectedReason string, t *testing.T) {
var (
payload = []byte(t.Name())
)
sessionID := uuid.New()
cfdConn, originConn := net.Pipe()
// Registers and run a new session
session, err := qc.sessionManager.RegisterSession(ctx, sessionID, cfdConn)
session, err := datagramConn.sessionManager.RegisterSession(ctx, sessionID, cfdConn)
require.NoError(t, err)
sessionDone := make(chan struct{})
go func() {
qc.serveUDPSession(session, time.Millisecond*50)
datagramConn.serveUDPSession(session, time.Millisecond*50)
close(sessionDone)
}()
// Send a message to the quic session on edge side, it should be deumx to this datagram v2 session
muxedPayload, err := quicpogs.SuffixSessionID(sessionID, payload)
muxedPayload, err := cfdquic.SuffixSessionID(sessionID, payload)
require.NoError(t, err)
muxedPayload, err = quicpogs.SuffixType(muxedPayload, quicpogs.DatagramTypeUDP)
muxedPayload, err = cfdquic.SuffixType(muxedPayload, cfdquic.DatagramTypeUDP)
require.NoError(t, err)
err = edgeQUICSession.SendMessage(muxedPayload)
err = edgeQUICSession.SendDatagram(muxedPayload)
require.NoError(t, err)
readBuffer := make([]byte, len(payload)+1)
@@ -637,7 +656,7 @@ func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.
case closedByOrigin:
originConn.Close()
case closedByRemote:
err = qc.UnregisterUdpSession(ctx, sessionID, expectedReason)
err = datagramConn.UnregisterUdpSession(ctx, sessionID, expectedReason)
require.NoError(t, err)
case closedByTimeout:
}
@@ -666,7 +685,7 @@ const (
closedByTimeout
)
func runRPCServer(ctx context.Context, session quic.Connection, sessionRPCServer tunnelpogs.SessionManager, configRPCServer tunnelpogs.ConfigurationManager, t *testing.T) {
func runRPCServer(ctx context.Context, session quic.Connection, sessionRPCServer pogs.SessionManager, configRPCServer pogs.ConfigurationManager, t *testing.T) {
stream, err := session.AcceptStream(ctx)
require.NoError(t, err)
@@ -675,13 +694,15 @@ func runRPCServer(ctx context.Context, session quic.Connection, sessionRPCServer
stream, err = session.AcceptStream(ctx)
require.NoError(t, err)
}
protocol, err := quicpogs.DetermineProtocol(stream)
assert.NoError(t, err)
rpcServerStream, err := quicpogs.NewRPCServerStream(stream, protocol)
assert.NoError(t, err)
log := zerolog.New(os.Stdout)
err = rpcServerStream.Serve(sessionRPCServer, configRPCServer, &log)
ss := rpcquic.NewCloudflaredServer(
func(_ context.Context, _ *rpcquic.RequestServerStream) error {
return nil
},
sessionRPCServer,
configRPCServer,
10*time.Second,
)
err = ss.Serve(ctx, stream)
assert.NoError(t, err)
}
@@ -706,27 +727,59 @@ func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionI
return nil
}
func testQUICConnection(udpListenerAddr net.Addr, t *testing.T, index uint8) *QUICConnection {
func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8) (TunnelConnection, *datagramV2Connection) {
tlsClientConfig := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"argotunnel"},
}
// Start a mock httpProxy
log := zerolog.New(os.Stdout)
qc, err := NewQUICConnection(
log := zerolog.New(io.Discard)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Dial the QUIC connection to the edge
conn, err := DialQuic(
ctx,
testQUICConfig,
udpListenerAddr,
nil,
index,
tlsClientConfig,
&mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}},
&tunnelpogs.ConnectionOptions{},
fakeControlStream{},
serverAddr,
nil, // connect on a random port
index,
&log,
)
// Start a session manager for the connection
sessionDemuxChan := make(chan *packet.Session, 4)
datagramMuxer := cfdquic.NewDatagramMuxerV2(conn, &log, sessionDemuxChan)
sessionManager := datagramsession.NewManager(&log, datagramMuxer.SendToSession, sessionDemuxChan)
var connIndex uint8 = 0
packetRouter := ingress.NewPacketRouter(nil, datagramMuxer, connIndex, &log)
datagramConn := &datagramV2Connection{
conn,
sessionManager,
datagramMuxer,
packetRouter,
15 * time.Second,
0 * time.Second,
&log,
}
tunnelConn, err := NewTunnelConnection(
ctx,
conn,
index,
&mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}},
datagramConn,
fakeControlStream{},
&pogs.ConnectionOptions{},
15*time.Second,
0*time.Second,
0*time.Second,
&log,
nil,
)
require.NoError(t, err)
return qc
return tunnelConn, datagramConn
}
type mockReaderNoopWriter struct {
@@ -740,3 +793,27 @@ func (m *mockReaderNoopWriter) Write(p []byte) (n int, err error) {
func (m *mockReaderNoopWriter) Close() error {
return nil
}
// GenerateTLSConfig sets up a bare-bones TLS config for a QUIC server
func GenerateTLSConfig() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
NextProtos: []string{"argotunnel"},
}
}
+201
View File
@@ -0,0 +1,201 @@
package connection
import (
"context"
"fmt"
"net"
"time"
"github.com/google/uuid"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/datagramsession"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/management"
"github.com/cloudflare/cloudflared/packet"
cfdquic "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/tracing"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
)
const (
// emperically this capacity has been working well
demuxChanCapacity = 16
)
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
// connection streams.
type DatagramSessionHandler interface {
Serve(context.Context) error
pogs.SessionManager
}
type datagramV2Connection struct {
conn quic.Connection
// sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer
sessionManager datagramsession.Manager
// datagramMuxer mux/demux datagrams from quic connection
datagramMuxer *cfdquic.DatagramMuxerV2
packetRouter *ingress.PacketRouter
rpcTimeout time.Duration
streamWriteTimeout time.Duration
logger *zerolog.Logger
}
func NewDatagramV2Connection(ctx context.Context,
conn quic.Connection,
icmpRouter ingress.ICMPRouter,
index uint8,
rpcTimeout time.Duration,
streamWriteTimeout time.Duration,
logger *zerolog.Logger,
) DatagramSessionHandler {
sessionDemuxChan := make(chan *packet.Session, demuxChanCapacity)
datagramMuxer := cfdquic.NewDatagramMuxerV2(conn, logger, sessionDemuxChan)
sessionManager := datagramsession.NewManager(logger, datagramMuxer.SendToSession, sessionDemuxChan)
packetRouter := ingress.NewPacketRouter(icmpRouter, datagramMuxer, index, logger)
return &datagramV2Connection{
conn,
sessionManager,
datagramMuxer,
packetRouter,
rpcTimeout,
streamWriteTimeout,
logger,
}
}
func (d *datagramV2Connection) Serve(ctx context.Context) error {
// If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits
// as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this
// connection).
// If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the
// other goroutine as fast as possible.
ctx, cancel := context.WithCancel(ctx)
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
defer cancel()
return d.sessionManager.Serve(ctx)
})
errGroup.Go(func() error {
defer cancel()
return d.datagramMuxer.ServeReceive(ctx)
})
errGroup.Go(func() error {
defer cancel()
return d.packetRouter.Serve(ctx)
})
return errGroup.Wait()
}
// RegisterUdpSession is the RPC method invoked by edge to register and run a session
func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*tunnelpogs.RegisterUdpSessionResponse, error) {
traceCtx := tracing.NewTracedContext(ctx, traceContext, q.logger)
ctx, registerSpan := traceCtx.Tracer().Start(traceCtx, "register-session", trace.WithAttributes(
attribute.String("session-id", sessionID.String()),
attribute.String("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)),
))
log := q.logger.With().Int(management.EventTypeKey, int(management.UDP)).Logger()
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
originProxy, err := ingress.DialUDP(dstIP, dstPort)
if err != nil {
log.Err(err).Msgf("Failed to create udp proxy to %s:%d", dstIP, dstPort)
tracing.EndWithErrorStatus(registerSpan, err)
return nil, err
}
registerSpan.SetAttributes(
attribute.Bool("socket-bind-success", true),
attribute.String("src", originProxy.LocalAddr().String()),
)
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
if err != nil {
originProxy.Close()
log.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).Msgf("Failed to register udp session")
tracing.EndWithErrorStatus(registerSpan, err)
return nil, err
}
go q.serveUDPSession(session, closeAfterIdleHint)
log.Debug().
Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).
Str("src", originProxy.LocalAddr().String()).
Str("dst", fmt.Sprintf("%s:%d", dstIP, dstPort)).
Msgf("Registered session")
tracing.End(registerSpan)
resp := tunnelpogs.RegisterUdpSessionResponse{
Spans: traceCtx.GetProtoSpans(),
}
return &resp, nil
}
// UnregisterUdpSession is the RPC method invoked by edge to unregister and terminate a sesssion
func (q *datagramV2Connection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
return q.sessionManager.UnregisterSession(ctx, sessionID, message, true)
}
func (q *datagramV2Connection) serveUDPSession(session *datagramsession.Session, closeAfterIdleHint time.Duration) {
ctx := q.conn.Context()
closedByRemote, err := session.Serve(ctx, closeAfterIdleHint)
// If session is terminated by remote, then we know it has been unregistered from session manager and edge
if !closedByRemote {
if err != nil {
q.closeUDPSession(ctx, session.ID, err.Error())
} else {
q.closeUDPSession(ctx, session.ID, "terminated without error")
}
}
q.logger.Debug().Err(err).
Int(management.EventTypeKey, int(management.UDP)).
Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(session.ID)).
Msg("Session terminated")
}
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
func (q *datagramV2Connection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) {
q.sessionManager.UnregisterSession(ctx, sessionID, message, false)
quicStream, err := q.conn.OpenStream()
if err != nil {
// Log this at debug because this is not an error if session was closed due to lost connection
// with edge
q.logger.Debug().Err(err).
Int(management.EventTypeKey, int(management.UDP)).
Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).
Msgf("Failed to open quic stream to unregister udp session with edge")
return
}
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
defer stream.Close()
rpcClientStream, err := rpcquic.NewSessionClient(ctx, stream, q.rpcTimeout)
if err != nil {
// Log this at debug because this is not an error if session was closed due to lost connection
// with edge
q.logger.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).
Msgf("Failed to open rpc stream to unregister udp session with edge")
return
}
defer rpcClientStream.Close()
if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil {
q.logger.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).
Msgf("Failed to unregister udp session with edge")
}
}
+58
View File
@@ -0,0 +1,58 @@
package connection
import (
"context"
"fmt"
"net"
"time"
"github.com/google/uuid"
"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/management"
cfdquic "github.com/cloudflare/cloudflared/quic/v3"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
type datagramV3Connection struct {
conn quic.Connection
// datagramMuxer mux/demux datagrams from quic connection
datagramMuxer cfdquic.DatagramConn
logger *zerolog.Logger
}
func NewDatagramV3Connection(ctx context.Context,
conn quic.Connection,
sessionManager cfdquic.SessionManager,
icmpRouter ingress.ICMPRouter,
index uint8,
metrics cfdquic.Metrics,
logger *zerolog.Logger,
) DatagramSessionHandler {
log := logger.
With().
Int(management.EventTypeKey, int(management.UDP)).
Uint8(LogFieldConnIndex, index).
Logger()
datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
return &datagramV3Connection{
conn,
datagramMuxer,
logger,
}
}
func (d *datagramV3Connection) Serve(ctx context.Context) error {
return d.datagramMuxer.Serve(ctx)
}
func (d *datagramV3Connection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration, traceContext string) (*pogs.RegisterUdpSessionResponse, error) {
return nil, fmt.Errorf("datagram v3 does not support RegisterUdpSession RPC")
}
func (d *datagramV3Connection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
return fmt.Errorf("datagram v3 does not support UnregisterUdpSession RPC")
}

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