Compare commits

..

155 Commits

Author SHA1 Message Date
Adam Chalmers 293b9af4a7 Release 2020.11.8 2020-11-17 17:15:50 -06:00
Adam Chalmers 029f7e0378 TUN-3555: Single origin service should default to localhost:8080 2020-11-17 23:12:32 +00:00
Adam Chalmers 58c5e25b9a Release 2020.11.7 2020-11-16 14:13:04 -06:00
Adam Chalmers 25e72f7760 TUN-3549: Use a separate handler for each websocket proxy 2020-11-16 20:05:35 +00:00
Adam Chalmers 7613410855 TUN-3548, TUN-3547: Bastion mode can be specified as a service, doesn't
require URL.
2020-11-16 20:04:36 +00:00
cthuang c40cb7dc56 TUN-3514: Stop setting --is-autoupdated flag after autoupdate because it can break named tunnel running in k8s 2020-11-16 09:40:38 +00:00
Adam Chalmers 9ae5f306bf Release 2020.11.6 2020-11-14 20:13:18 -06:00
Adam Chalmers 6159cb536f TUN-3546: Fix panic in tlsconfig.LoadOriginCA 2020-11-14 20:03:29 -06:00
Adam Chalmers ae4f687754 Release 2020.11.5 2020-11-13 17:39:51 -06:00
Adam Chalmers ce7d0572fe TUN-3543: ProxyAddress not using default in single-origin mode 2020-11-13 17:27:55 -06:00
Troy Varney 030b768eeb DEVTOOLS-7936: Set permissions on public packages
This updates the public repository upload process to change the group on
the uploaded files to `cf` and adds the write permission for members of
the group. This should allow the `cf` user to properly overwrite the
file when signing it.
2020-11-13 19:02:40 +00:00
Adam Chalmers f36dc6cfd8 TUN-3540: Better copy in ingress rules error messages 2020-11-12 17:57:19 -06:00
cthuang 6dad2bf9c4 Release 2020.11.4 2020-11-12 18:09:09 +00:00
Adam Chalmers 1475cf61ee TUN-3534: Specific error message when credentials file is a .pem not .json 2020-11-12 16:38:24 +00:00
cthuang ebc003d478 TUN-3514: Transport logger write to UI when UI is enabled 2020-11-11 15:21:00 +00:00
cthuang 543169c893 TUN-3490: Make sure OriginClient implementation doesn't write after Proxy return 2020-11-11 15:21:00 +00:00
cthuang d5769519b2 TUN-3489: Add unit tests to cover proxy logic in connection package of cloudflared 2020-11-11 15:21:00 +00:00
cthuang 5974fb4cfd TUN-3500: Integrate replace h2mux by http2 work with multiple origin support 2020-11-11 15:20:57 +00:00
cthuang eef5b78eac TUN-3480: Support SSE with http2 connection, and add SSE handler to hello-world server 2020-11-11 15:12:17 +00:00
cthuang 6b86f81c4a TUN-3403: Unit test for origin/proxy to test serving HTTP and Websocket 2020-11-11 15:12:15 +00:00
cthuang a490443630 TUN-3458: Upgrade to http2 when available, fallback to h2mux when we reach max retries 2020-11-11 15:11:42 +00:00
cthuang b5cdf3b2c7 TUN-3456: New protocol option auto to automatically select between http2 and h2mux 2020-11-11 15:11:42 +00:00
cthuang 6886e5f90a TUN-3467: Serialize cf-cloudflared-response-meta during package initialization using jsoniter 2020-11-11 15:11:42 +00:00
cthuang 9ac40dcf04 TUN-3462: Refactor cloudflared to separate origin from connection 2020-11-11 15:11:42 +00:00
cthuang a5a5b93b64 TUN-3420: Establish control plane and send RPC over control plane 2020-11-11 15:11:42 +00:00
cthuang cb39f26f27 TUN-3406: Proxy websocket requests over Go http2 2020-11-11 15:11:42 +00:00
cthuang 8d7b2575ba TUN-3400: Use Go HTTP2 library as transport to connect with the edge 2020-11-11 15:11:42 +00:00
cthuang d7498b0c03 TUN-3449: Use flag to select transport protocol implementation 2020-11-11 15:11:42 +00:00
Adam Chalmers 59051ba425 Release 2020.11.3 2020-11-11 08:52:04 -06:00
cthuang d7268af555 TUN-3533: Set config for single origin ingress 2020-11-11 13:54:20 +00:00
Adam Chalmers 8c1deb4064 Release 2020.11.2 2020-11-10 17:02:12 -06:00
Adam Chalmers 986102401b Release 2020.11.1 2020-11-10 15:43:33 -06:00
Adam Chalmers 196762d9d3 TUN-3527: More specific error for invalid YAML/JSON 2020-11-10 21:42:26 +00:00
Samuel Rhea 350a6f2bf5 Update README.md (#256)
* Update README.md

* Update README.md

* Update README.md
2020-11-10 21:36:25 +00:00
Adam Chalmers 3de3b2f734 Release 2020.11.0 2020-11-10 09:44:29 -06:00
Adam Chalmers 4698ec8dee TUN-3461: Show all origin services in the UI 2020-11-10 14:25:37 +00:00
Igor Postelnik 8c6181db9f TUN-3524: Don't ignore errors from app-level action handler (#248) 2020-11-10 13:06:49 +00:00
Adam Chalmers 64d3836645 TUN-3522: ingress validate checks that the config file exists 2020-11-09 12:31:50 -06:00
Adam Chalmers 87e2679744 TUN-3516: Better error message when parsing invalid YAML config 2020-11-09 10:35:28 -06:00
cthuang 61c814bd79 TUN-3506: OriginService needs to set request host and scheme for websocket requests 2020-11-05 16:34:02 +00:00
cthuang be9a558867 TUN-3503: Matching ingress rule should not take port into account 2020-11-05 15:36:12 +00:00
cthuang b00bfe8e0b TUN-3505: Response body for status code origin returns EOF on Read 2020-11-05 14:47:46 +00:00
Adam Chalmers bc015995d8 TUN-3484: OriginService that responds with configured HTTP status 2020-11-04 21:28:33 +00:00
Adam Chalmers d01770107e TUN-3492: Refactor OriginService, shrink its interface 2020-11-04 21:28:33 +00:00
cthuang 18c359cb86 TUN-3494: Proceed to create tunnel if at least one edge address can be resolved 2020-10-30 18:18:34 +00:00
Adam Chalmers e933ef9e1a TUN-2640: Users can configure per-origin config. Unify single-rule CLI
flow with multi-rule config file code.
2020-10-30 07:42:20 -05:00
cthuang ea71b78e6d TUN-3478: Increase download timeout to 60s 2020-10-22 10:38:18 +01:00
Igor Postelnik 04fa10ea7b Release 2020.10.2 2020-10-21 13:42:12 -05:00
Dalton 8e0be7cff1 AUTH-3185 fixed indention error 2020-10-21 12:14:39 -05:00
cthuang 3c13080fe1 Release 2020.10.1 2020-10-21 17:09:19 +01:00
Igor Postelnik b6cd54d854 TUN-3459: Make service install on linux use named tunnels 2020-10-21 10:46:29 -05:00
cthuang f0cfad8efa TUN-3476: Fix conversion to string and int slice 2020-10-21 16:03:25 +01:00
Igor Postelnik ed54d150fe Move raw ingress rules to config package 2020-10-20 12:00:34 -05:00
Igor Postelnik ca4887fb19 Split out typed config from legacy command-line switches; refactor ingress commands and fix tests 2020-10-20 10:10:19 -05:00
Igor Postelnik eaf03305bd TUN-3475: Unify config file handling with typed config for new fields 2020-10-20 08:55:30 -05:00
Igor Postelnik 051908aaef TUN-3463: Let users run a named tunnel via config file setting 2020-10-19 12:27:18 +00:00
Adam Chalmers acd03e36e6 TUN-3465: Use Go 1.15.3 2020-10-15 15:55:16 -05:00
Adam Chalmers c96b9e8d8f TUN-3464: Newtype to wrap []ingress.Rule 2020-10-15 12:48:14 -05:00
Adam Chalmers 4a4a1bb6b1 TUN-3441: Multiple-origin routing via ingress rules 2020-10-13 08:55:17 -05:00
Adam Chalmers 0eebc7cef9 TUN-3438: move ingress into own package, read into TunnelConfig 2020-10-12 16:33:22 +00:00
Igor Postelnik 53a1fa46a8 TUN-3452: Fix loading of flags from config file for tunnel run subcommand. This change also cleans up building of tunnel subcommand list, hides deprecated subcommands and improves help. 2020-10-09 12:07:17 -05:00
Adam Chalmers 86a7af3dc4 TUN-3451: Cloudflared tunnel ingress command 2020-10-08 22:06:40 +00:00
Adam Chalmers 407c9550d7 TUN-3440: 'tunnel rule' command to test ingress rules 2020-10-08 22:06:40 +00:00
Adam Chalmers 2319003e10 TUN-3439: 'tunnel validate' command to check ingress rules 2020-10-08 22:06:40 +00:00
cthuang 1e6399c2f0 TUN-3446: Use go 1.15.2 and add a step to build cloudflared in the dev Dockerfile 2020-10-07 21:28:54 +00:00
Adam Chalmers b05d826d22 TUN-3436, TUN-3437: Parse ingress from YAML, ensure last rule catches everything 2020-10-07 16:36:28 +00:00
Dalton d21989dba4 AUTH-3148 fixed cloudflared copy and match all the files in the checksum upload 2020-10-06 11:39:40 -05:00
Igor Postelnik a986355a61 Release 2020.10.0 2020-10-06 09:54:04 -05:00
cthuang 812244d79f TUN-3443: Decode as v4api response on non-200 status 2020-10-06 11:34:41 +00:00
Dalton be7b7c7149 AUTH-2993 cleaned up worker service tests 2020-10-02 13:01:05 -05:00
cthuang 03d7320a44 TUN-3430: Copy flags to configure proxy to run subcommand, print relevant tunnel flags in help 2020-10-01 21:44:27 +00:00
Dalton ba4c8d8849 AUTH-2993 added workers updater logic 2020-10-01 14:41:58 -05:00
cthuang 2c9b7361b7 TUN-3427: Define a struct that only implements RegistrationServer in tunnelpogs 2020-10-01 09:08:32 +01:00
Lee Valentine 8e8513e325 TRAFFIC-448: allow the user to specify the proxy address and port to bind to, falling back to 127.0.0.1 and random port if not specified 2020-09-25 09:54:40 -05:00
Adam Chalmers 607dcff697 Release 2020.9.3 2020-09-24 12:07:27 -05:00
Adam Chalmers 1d1cf6a827 Release 2020.9.2 2020-09-24 19:23:53 +04:00
Adam Chalmers 61f3fab757 TUN-3410: Request the v1 Tunnelstore API 2020-09-24 19:23:53 +04:00
Lee Valentine e2ff7f65fc TRAFFIC-448: build cloudflare for junos and publish to s3 2020-09-24 19:23:53 +04:00
Dalton da0792a321 AUTH-3109 upload the checksum to workers kv on github releases 2020-09-23 18:01:43 +04:00
Bojan Zelic fa061ab54e updater service exit code should be 11 2020-09-22 18:26:56 +04:00
cthuang 197d65659a TUN-3291: cloudflared tunnel run -h explains how to use flags from parent command 2020-09-21 19:07:30 +00:00
Igor Postelnik 85d0afd3b0 TUN-3295: Show route command results 2020-09-21 16:32:08 +00:00
Michael Borkenstein 20623255dd AUTH-3110-use-cfsetup-precache 2020-09-18 16:26:33 -05:00
Dalton afa5e68fe5 AUTH-3103 CI build fixes 2020-09-18 19:33:30 +00:00
Areg Harutyunyan 747427f816 TUN-3216: UI improvements 2020-09-17 13:22:08 +04:00
Rachel Williams f99b6c6421 TUN-3335: Dynamically set connection table size for UI 2020-09-17 11:52:10 +04:00
Rachel Williams 250bc54110 TUN-3333: Add text to UI explaining how to exit 2020-09-17 11:52:10 +04:00
Rachel Williams bb530b87dd TUN-3328: Filter out free tunnel has started log from UI 2020-09-17 11:52:10 +04:00
Rachel Williams 02587c1edc TUN-3321: Add box around logs on UI 2020-09-17 11:52:10 +04:00
Rachel Williams 26fc20d406 TUN-3198: Handle errors while running tunnel UI 2020-09-17 11:52:10 +04:00
Rachel Williams 8a829b773a Rebased and passed TunnelEventChan to LogServerInfo in new ReconnectTunnel function 2020-09-17 11:52:10 +04:00
Rachel Williams 71b98e6111 TUN-3261: Display connections on UI for free classic tunnels 2020-09-17 11:52:10 +04:00
Rachel Williams fee13dc62f TUN-3255: Update UI to display URL instead of hostname 2020-09-17 11:52:10 +04:00
Rachel Williams 094e0c7592 TUN-3238: Update UI when connection re-connects 2020-09-17 11:52:10 +04:00
Rachel Williams b57a953caa TUN-3200: Add connection information to UI 2020-09-17 11:52:10 +04:00
Rachel Williams d8ebde37ca TUN-3201: Create base cloudflared UI structure 2020-09-17 11:52:07 +04:00
Adam Chalmers 0708a49848 Release 2020.9.1 2020-09-16 12:54:59 -05:00
Adam Chalmers b26f3082e6 Use Go 1.15.2 2020-09-16 12:45:49 -05:00
Igor Postelnik cb6d424765 TUN-3395: Improve help for list command 2020-09-16 16:48:43 +00:00
Igor Postelnik 5753aa9f18 TUN-3294: Perform basic validation on arguments of route command; remove default pool name which wasn't valid 2020-09-16 16:48:43 +00:00
Igor Postelnik bfae12008d TUN-3395: Improve help for list command 2020-09-16 08:30:24 -05:00
Igor Postelnik c52e0dc8ef TUN-3395: Unhide named tunnel subcommands, tweak help 2020-09-15 12:41:15 +00:00
Adam Chalmers 55346444c9 Release 2020.9.0 2020-09-14 16:41:59 -05:00
Igor Postelnik ba785ec58d TUN-3293: Try to use error information from the body of a failed tunnelstore reresponse if available 2020-09-11 17:12:17 -05:00
Adam Chalmers 3be2545ad4 TUN-3292: Mention cleanup in tunnel run help. 2020-09-10 11:02:31 -05:00
Dalton 4a8597c245 AUTH-2653 add notarization to mac build 2020-09-09 19:43:00 +00:00
Igor Postelnik 741cd66c9e TUN-3375: Upgrade coredns and prometheus dependencies 2020-09-09 13:14:38 -05:00
Adam Chalmers 7acea1ac99 TUN-3375: Upgrade x/text and gorilla websocket deps 2020-09-09 12:07:35 -05:00
cthuang 22d771b51d TUN-3284: Use cloudflared/<version> as user agent of tunnelstore client 2020-09-09 10:34:26 +01:00
Dalton 00d6ab2eb7 AUTH-2653 search for the certificate and not the identity 2020-09-08 12:21:10 -05:00
Dalton cc0a5ac3df AUTH-2653 disble error pipe to see what is failing 2020-09-08 10:41:06 -05:00
cthuang 5fb938d6d6 TUN-3345: tunnel run accepts name of tunnel as argument 2020-09-05 09:32:50 +00:00
Dalton cd5bdb837e AUTH-2653 handle duplicate key import errors 2020-09-04 17:08:09 -05:00
Dalton 107abf9d29 AUTH-2653 changed to proper file extension 2020-09-04 20:23:42 +00:00
Adam Chalmers 218ee30206 TUN-3377: Tunnel route should check dns/lb before checking tunnel ID 2020-09-04 18:44:06 +00:00
Dalton 8764fbfdfa AUTH-2653 renabled signing 2020-09-04 11:29:17 -05:00
Dalton 2463d6b92c TUN-3352 extra debug logging for websockets 2020-08-31 17:14:02 -05:00
Dalton 810d268c99 AUTH-2016 fixed variable fail 2020-08-20 12:56:12 -05:00
Dalton f5b479dbbc Release 2020.8.2 2020-08-20 11:15:11 -05:00
Dalton b52728e829 AUTH-3021 fixed the git version call by using the older flag 2020-08-20 16:13:10 +00:00
cthuang 8eeb452cce TUN-3268: Each connection has its own event digest to reconnect 2020-08-20 16:44:22 +01:00
Dalton 9323844ea7 Release 2020.8.1 2020-08-19 21:59:21 -05:00
Dalton b698fe5ef3 AUTH-2864 - add macos build to github release 2020-08-19 21:31:50 -05:00
Dalton edb980d439 AUTH-2653 disabling signing until we can get the keys 2020-08-19 15:32:49 -05:00
Dalton 310bd0dbf1 AUTH-2712 fixed the mac build script 2020-08-19 14:57:43 +00:00
Adam Chalmers 1a96889141 TUN-3286: Use either ID or name in Named Tunnel subcommands. 2020-08-19 14:39:45 +00:00
Dalton Cherry 60de05bfc1 AUTH-2712 added MSI build for a windows agent 2020-08-17 14:44:28 -05:00
cthuang 3deef6197f TUN-3213: Create, route and run named tunnels in one command 2020-08-17 19:38:38 +00:00
Dalton 70114c2145 AUTH-2977 log file protection 2020-08-17 17:55:58 +00:00
Dalton 5499c77e62 AUTH-2975 don't check /etc on windows 2020-08-17 12:40:36 -05:00
cthuang 292a7f07a2 TUN-3243: Refactor tunnel subcommands to allow commands to compose better 2020-08-11 10:02:52 +01:00
Igor Postelnik 679f36303a TUN-3242: Build with go 1.14 2020-08-10 23:17:23 +00:00
Adam Chalmers 1b61d699c4 TUN-3237: By default, don't show connections that are pending reconnect 2020-08-07 10:16:13 -05:00
cthuang a7562dff68 TUN-3233: List tunnels support filtering by deleted, name, existed at and id 2020-08-07 10:09:26 +01:00
Dalton 8daf1e882f AUTH-2857 update homebrew script to use new url 2020-08-04 16:47:10 -05:00
Michael Borkenstein 20466dacb7 Release 2020.8.0 2020-08-03 12:18:44 -06:00
Dalton 5b3b592108 AUTH-2927 run message update after all github builds are done 2020-08-03 10:12:03 -05:00
Adam Chalmers 9952ce0069 TUN-3221: ConnectionOptions tracks numPreviousAttempts. 2020-07-31 20:22:33 +00:00
cthuang 1cbc8fb8ac TUN-3220: tunnel route reports created route 2020-07-31 17:48:58 +01:00
Michael Borkenstein edc69694cb AUTH-2714: Adds arm64 cloudflared build 2020-07-29 13:12:57 -06:00
Igor Postelnik bd15c6b8c3 TUN-3208: Reduce copies and allocations on h2mux write path. Pre-allocate 16KB write buffer on the first write if possible. Use explicit byte array for chunks on write thread to avoid copying through intermediate buffer due to io.CopyN.
benchmark                                    old ns/op     new ns/op     delta
BenchmarkSingleStreamLargeResponseBody-8     17786594      12163494      -31.61%

benchmark                                    old allocs     new allocs     delta
BenchmarkSingleStreamLargeResponseBody-8     17086          15869          -7.12%

benchmark                                    old bytes     new bytes     delta
BenchmarkSingleStreamLargeResponseBody-8     58215169      21604391      -62.89%
2020-07-29 14:30:12 +00:00
Igor Postelnik 42fe2e7266 TUN-3208: Add benchmark for large response write 2020-07-29 14:30:12 +00:00
Igor Postelnik 44e3be2c88 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
benchmark                                    old ns/op     new ns/op     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     10360         5048          -51.27%

benchmark                                    old allocs     new allocs     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     135            26             -80.74%

benchmark                                    old bytes     new bytes     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     8543          3667          -57.08%
2020-07-29 14:29:07 +00:00
Igor Postelnik 61d5461138 TUN-3209: Add benchmark for header serialization 2020-07-29 14:29:07 +00:00
Michael Borkenstein b696ca8b1c Removes centos 6 build 2020-07-29 14:01:34 +00:00
Dalton 6a4d020c27 AUTH-2016 adds sha256 hashes to releases 2020-07-27 10:36:31 -05:00
Igor Postelnik 4791ba3b87 TUN-3194: Don't render log output when level is not enabled 2020-07-23 18:36:31 -05:00
Igor Postelnik cf1c9a3083 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging 2020-07-21 18:07:50 -05:00
Igor Postelnik d61e3fb130 TUN-3190: Initialize logger using command line flags in tunnels subcommands 2020-07-21 17:25:48 +00:00
Dalton ca7d6797e1 AUTH-2902 redirect with just the root host on curl commands 2020-07-21 11:08:31 -05:00
Areg Harutyunyan af0d04d0f3 Release 2020.7.4 2020-07-21 11:02:29 +01:00
Ivan Babrou 6274567e16 Build cloudflared for arm64 on native agents 2020-07-20 17:42:50 -07:00
cthuang 8836ee1dda TUN-3156: Add route subcommand under tunnel 2020-07-17 05:51:24 +08:00
Michael Borkenstein 7afde79600 AUTH-2890: adds error handler to cli actions 2020-07-15 14:33:36 +00:00
Rachel Williams 3d782f7162 TUN-3048: Handle error when user tries to delete active tunnel 2020-07-14 15:32:46 +00:00
1340 changed files with 180119 additions and 45859 deletions
+3
View File
@@ -5,12 +5,15 @@ guide/public
/.GOPATH
/bin
.idea
.build
.vscode
\#*\#
cscope.*
cloudflared
cloudflared.pkg
cloudflared.exe
cloudflared.msi
cloudflared-x86-64*
!cmd/cloudflared/
.DS_Store
*-session.log
+169 -3
View File
@@ -1,7 +1,5 @@
#!/bin/bash
set -euo pipefail
if [[ "$(uname)" != "Darwin" ]] ; then
echo "This should be run on macOS"
exit 1
@@ -13,9 +11,177 @@ export GO111MODULE=on
# build 'cloudflared-darwin-amd64.tgz'
mkdir -p artifacts
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
PKGNAME="$(pwd)/artifacts/cloudflared-amd64.pkg"
TARGET_DIRECTORY=".build"
BINARY_NAME="cloudflared"
VERSION=$(git describe --tags --always --dirty="-dev")
PRODUCT="cloudflared"
CODE_SIGN_PRIV="code_sign.p12"
CODE_SIGN_CERT="code_sign.cer"
INSTALLER_PRIV="installer.p12"
INSTALLER_CERT="installer.cer"
BUNDLE_ID="com.cloudflare.cloudflared"
SEC_DUP_MSG="security: SecKeychainItemImport: The specified item already exists in the keychain."
export PATH="$PATH:/usr/local/bin"
mkdir -p ../src/github.com/cloudflare/
cp -r . ../src/github.com/cloudflare/cloudflared
cd ../src/github.com/cloudflare/cloudflared
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
tar czf "$FILENAME" cloudflared
# Add code signing private key to the key chain
if [[ ! -z "$CFD_CODE_SIGN_KEY" ]]; then
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
# write private key to disk and then import it keychain
echo -n -e ${CFD_CODE_SIGN_KEY} | base64 -D > ${CODE_SIGN_PRIV}
out=$(security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}" 2>&1)
exitcode=$?
if [ -n "$out" ]; then
if [ $exitcode -eq 0 ]; then
echo "$out"
else
if [ "$out" != "${SEC_DUP_MSG}" ]; then
echo "$out" >&2
exit $exitcode
fi
fi
fi
rm ${CODE_SIGN_PRIV}
fi
fi
# Add code signing certificate to the key chain
if [[ ! -z "$CFD_CODE_SIGN_CERT" ]]; then
# write certificate to disk and then import it keychain
echo -n -e ${CFD_CODE_SIGN_CERT} | base64 -D > ${CODE_SIGN_CERT}
out1=$(security import ${CODE_SIGN_CERT} -A 2>&1)
exitcode1=$?
if [ -n "$out1" ]; then
if [ $exitcode1 -eq 0 ]; then
echo "$out1"
else
if [ "$out1" != "${SEC_DUP_MSG}" ]; then
echo "$out1" >&2
exit $exitcode1
else
echo "already imported code signing certificate"
fi
fi
fi
rm ${CODE_SIGN_CERT}
fi
# Add package signing private key to the key chain
if [[ ! -z "$CFD_INSTALLER_KEY" ]]; then
if [[ ! -z "$CFD_INSTALLER_PASS" ]]; then
# write private key to disk and then import it into the keychain
echo -n -e ${CFD_INSTALLER_KEY} | base64 -D > ${INSTALLER_PRIV}
out2=$(security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}" 2>&1)
exitcode2=$?
if [ -n "$out2" ]; then
if [ $exitcode2 -eq 0 ]; then
echo "$out2"
else
if [ "$out2" != "${SEC_DUP_MSG}" ]; then
echo "$out2" >&2
exit $exitcode2
fi
fi
fi
rm ${INSTALLER_PRIV}
fi
fi
# Add package signing certificate to the key chain
if [[ ! -z "$CFD_INSTALLER_CERT" ]]; then
# write certificate to disk and then import it keychain
echo -n -e ${CFD_INSTALLER_CERT} | base64 -D > ${INSTALLER_CERT}
out3=$(security import ${INSTALLER_CERT} -A 2>&1)
exitcode3=$?
if [ -n "$out3" ]; then
if [ $exitcode3 -eq 0 ]; then
echo "$out3"
else
if [ "$out3" != "${SEC_DUP_MSG}" ]; then
echo "$out3" >&2
exit $exitcode3
else
echo "already imported installer certificate"
fi
fi
fi
rm ${INSTALLER_CERT}
fi
# get the code signing certificate name
if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then
CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
else
if [[ -n "$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
else
CODE_SIGN_NAME=""
fi
fi
# get the package signing certificate name
if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then
PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
else
if [[ -n "$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
else
PKG_SIGN_NAME=""
fi
fi
# sign the cloudflared binary
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
# notarize the binary
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
zip "${BINARY_NAME}.zip" ${BINARY_NAME}
xcrun altool --notarize-app -f "${BINARY_NAME}.zip" -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
fi
fi
# creating build directory
rm -rf $TARGET_DIRECTORY
mkdir "${TARGET_DIRECTORY}"
mkdir "${TARGET_DIRECTORY}/contents"
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
# copy cloudflared into the build directory
cp ${BINARY_NAME} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
# compress cloudflared into a tar and gzipped file
tar czf "$FILENAME" "${BINARY_NAME}"
# build the installer package
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
--sign "${PKG_SIGN_NAME}" \
${PKGNAME}
# notarize the package
if [[ ! -z "$CFD_NOTE_PASSWORD" ]]; then
xcrun altool --notarize-app -f ${PKGNAME} -t osx -u ${CFD_NOTE_USERNAME} -p ${CFD_NOTE_PASSWORD} --primary-bundle-id ${BUNDLE_ID}
xcrun stapler staple ${PKGNAME}
fi
else
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PKGNAME}
fi
# cleaning up the build directory
rm -rf $TARGET_DIRECTORY
+1 -1
View File
@@ -43,7 +43,7 @@ git checkout -f master
git reset --hard origin/master
# modify cloudflared.rb
URL="https://developers.cloudflare.com/argo-tunnel/dl/cloudflared-$VERSION-darwin-amd64.tgz"
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
tee cloudflared.rb <<EOF
class Cloudflared < Formula
desc 'Argo Tunnel'
+1 -1
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.13.3 as builder
FROM golang:1.15.3 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
+74 -3
View File
@@ -1,6 +1,9 @@
VERSION := $(shell git describe --tags --always --dirty="-dev")
VERSION := $(shell git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
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.
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
IMPORT_PATH := github.com/cloudflare/cloudflared
PACKAGE_DIR := $(CURDIR)/packaging
@@ -22,6 +25,8 @@ ifneq ($(GOARCH),)
TARGET_ARCH ?= $(GOARCH)
else ifeq ($(LOCAL_ARCH),x86_64)
TARGET_ARCH ?= amd64
else ifeq ($(LOCAL_ARCH),i686)
TARGET_ARCH ?= amd64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
TARGET_ARCH ?= arm64
else ifeq ($(LOCAL_ARCH),aarch64)
@@ -39,6 +44,8 @@ else ifeq ($(LOCAL_OS),darwin)
TARGET_OS ?= darwin
else ifeq ($(LOCAL_OS),windows)
TARGET_OS ?= windows
else ifeq ($(LOCAL_OS),freebsd)
TARGET_OS ?= freebsd
else
$(error This system's OS $(LOCAL_OS) isn't supported)
endif
@@ -51,8 +58,6 @@ endif
ifeq ($(FLAVOR), centos-7)
TARGET_PUBLIC_REPO ?= el7
else ifeq ($(FLAVOR), centos-6)
TARGET_PUBLIC_REPO ?= el6
else
TARGET_PUBLIC_REPO ?= $(FLAVOR)
endif
@@ -84,6 +89,7 @@ define publish_package
for HOST in $(CF_PKG_HOSTS); do \
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
ssh cfsync@$$HOST 'chgrp cf /state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/*.$(1) && chmod g+w /state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/*.$(1)'; \
done
endef
@@ -122,6 +128,58 @@ 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
@@ -139,6 +197,15 @@ release: bin/equinox
github-release: cloudflared
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
.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
bin/equinox:
mkdir -p bin
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
@@ -156,3 +223,7 @@ vet:
go vet -mod=vendor ./...
which go-sumtype # go get github.com/BurntSushi/go-sumtype
go-sumtype $$(go list -mod=vendor ./...)
.PHONY: msi
msi: cloudflared
go-msi make --msi cloudflared.msi --version $(MSI_VERSION)
+26 -3
View File
@@ -1,9 +1,32 @@
# Argo Tunnel client
Contains the command-line client and its libraries for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network.
Contains the command-line client for Argo Tunnel, a tunneling daemon that proxies any local webserver through the Cloudflare network. Extensive documentation can be found in the [Argo Tunnel section](https://developers.cloudflare.com/argo-tunnel/) of the Cloudflare Docs.
## Getting started
## Before you get started
go install github.com/cloudflare/cloudflared/cmd/cloudflared
Before you use Argo Tunnel, you'll need to complete a few steps in the Cloudflare dashboard. The website you add to Cloudflare will be used to route traffic to your Tunnel.
1. [Add a website to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website)
2. [Change your domain nameservers to Cloudflare](https://support.cloudflare.com/hc/en-us/articles/205195708)
## Installing `cloudflared`
Downloads are available as standalone binaries, a Docker image, and Debian, RPM, and Homebrew packages. You can also find releases here on the `cloudflared` GitHub repository.
* You can [install on macOS](https://developers.cloudflare.com/argo-tunnel/getting-started/installation#macos) via Homebrew or by downloading the [latest Darwin amd64 release](https://github.com/cloudflare/cloudflared/releases)
* Binaries, Debian, and RPM packages for Linux [can be found here](https://developers.cloudflare.com/argo-tunnel/getting-started/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/argo-tunnel/getting-started/installation#windows)
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/argo-tunnel/
## Creating Tunnels and routing traffic
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels that serve traffic for hostnames in your account.
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/argo-tunnel/create-tunnel)
* Route traffic to that Tunnel with [DNS records in Cloudflare](https://developers.cloudflare.com/argo-tunnel/routing-to-tunnel/dns) or with a [Cloudflare Load Balancer](https://developers.cloudflare.com/argo-tunnel/routing-to-tunnel/lb)
## TryCloudflare
Want to test Argo Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/argo-tunnel/learning/trycloudflare).
+183
View File
@@ -1,3 +1,186 @@
2020.11.8
- 2020-11-17 TUN-3555: Single origin service should default to localhost:8080
2020.11.7
- 2020-11-13 TUN-3514: Stop setting --is-autoupdated flag after autoupdate because it can break named tunnel running in k8s
- 2020-11-15 TUN-3548, TUN-3547: Bastion mode can be specified as a service, doesn't require URL.
- 2020-11-16 TUN-3549: Use a separate handler for each websocket proxy
2020.11.6
- 2020-11-14 TUN-3546: Fix panic in tlsconfig.LoadOriginCA
2020.11.5
- 2020-11-12 TUN-3540: Better copy in ingress rules error messages
- 2020-11-12 DEVTOOLS-7936: Set permissions on public packages
- 2020-11-13 TUN-3543: ProxyAddress not using default in single-origin mode
2020.11.4
- 2020-11-11 TUN-3534: Specific error message when credentials file is a .pem not .json
- 2020-11-02 TUN-3500: Integrate replace h2mux by http2 work with multiple origin support
- 2020-11-09 TUN-3514: Transport logger write to UI when UI is enabled
- 2020-10-30 TUN-3490: Make sure OriginClient implementation doesn't write after Proxy return
- 2020-10-20 TUN-3403: Unit test for origin/proxy to test serving HTTP and Websocket
- 2020-10-23 TUN-3480: Support SSE with http2 connection, and add SSE handler to hello-world server
- 2020-10-27 TUN-3489: Add unit tests to cover proxy logic in connection package of cloudflared
- 2020-10-16 TUN-3467: Serialize cf-cloudflared-response-meta during package initialization using jsoniter
- 2020-10-14 TUN-3456: New protocol option auto to automatically select between http2 and h2mux
- 2020-10-14 TUN-3458: Upgrade to http2 when available, fallback to h2mux when we reach max retries
- 2020-10-08 TUN-3449: Use flag to select transport protocol implementation
- 2020-10-08 TUN-3462: Refactor cloudflared to separate origin from connection
- 2020-09-21 TUN-3406: Proxy websocket requests over Go http2
- 2020-09-25 TUN-3420: Establish control plane and send RPC over control plane
- 2020-09-11 TUN-3400: Use Go HTTP2 library as transport to connect with the edge
2020.11.3
- 2020-11-11 TUN-3533: Set config for single origin ingress
2020.11.2
2020.11.1
- 2020-11-10 TUN-3527: More specific error for invalid YAML/JSON
- 2020-11-06 Update README.md (#256)
2020.11.0
- 2020-11-04 TUN-3484: OriginService that responds with configured HTTP status
- 2020-11-05 TUN-3505: Response body for status code origin returns EOF on Read
- 2020-11-04 TUN-3503: Matching ingress rule should not take port into account
- 2020-11-05 TUN-3506: OriginService needs to set request host and scheme for websocket requests
- 2020-11-09 TUN-3516: Better error message when parsing invalid YAML config
- 2020-11-09 TUN-3522: ingress validate checks that the config file exists
- 2020-11-09 TUN-3524: Don't ignore errors from app-level action handler (#248)
- 2020-11-09 TUN-3461: Show all origin services in the UI
- 2020-10-30 TUN-3494: Proceed to create tunnel if at least one edge address can be resolved
- 2020-10-30 TUN-3492: Refactor OriginService, shrink its interface
- 2020-10-22 TUN-3478: Increase download timeout to 60s
- 2020-10-15 TUN-2640: Users can configure per-origin config. Unify single-rule CLI flow with multi-rule config file code.
2020.10.2
- 2020-10-21 Release 2020.10.1
- 2020-10-21 AUTH-3185 fixed indention error
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
2020.10.1
- 2020-10-20 Split out typed config from legacy command-line switches; refactor ingress commands and fix tests
- 2020-10-20 Move raw ingress rules to config package
- 2020-10-21 TUN-3476: Fix conversion to string and int slice
- 2020-10-12 TUN-3441: Multiple-origin routing via ingress rules
- 2020-10-15 TUN-3464: Newtype to wrap []ingress.Rule
- 2020-10-15 TUN-3465: Use Go 1.15.3
- 2020-10-15 TUN-3463: Let users run a named tunnel via config file setting
- 2020-10-19 TUN-3475: Unify config file handling with typed config for new fields
- 2020-10-19 TUN-3459: Make service install on linux use named tunnels
- 2020-10-06 AUTH-3148 fixed cloudflared copy and match all the files in the checksum upload
- 2020-10-06 TUN-3436, TUN-3437: Parse ingress from YAML, ensure last rule catches everything
- 2020-10-06 TUN-3446: Use go 1.15.2 and add a step to build cloudflared in the dev Dockerfile
- 2020-10-07 TUN-3439: 'tunnel validate' command to check ingress rules
- 2020-10-07 TUN-3440: 'tunnel rule' command to test ingress rules
- 2020-10-08 TUN-3451: Cloudflared tunnel ingress command
- 2020-10-09 TUN-3452: Fix loading of flags from config file for tunnel run subcommand. This change also cleans up building of tunnel subcommand list, hides deprecated subcommands and improves help.
- 2020-10-08 TUN-3438: move ingress into own package, read into TunnelConfig
2020.10.0
- 2020-10-02 AUTH-2993 cleaned up worker service tests
- 2020-10-02 TUN-3443: Decode as v4api response on non-200 status
- 2020-09-24 TRAFFIC-448: allow the user to specify the proxy address and port to bind to, falling back to 127.0.0.1 and random port if not specified
- 2020-09-28 TUN-3427: Define a struct that only implements RegistrationServer in tunnelpogs
- 2020-09-29 TUN-3430: Copy flags to configure proxy to run subcommand, print relevant tunnel flags in help
- 2020-08-12 AUTH-2993 added workers updater logic
2020.9.3
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
- 2020-09-23 Release 2020.9.2
- 2020-09-17 updater service exit code should be 11
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
2020.9.2
- 2020-09-22 TRAFFIC-448: build cloudflare for junos and publish to s3
- 2020-09-22 TUN-3410: Request the v1 Tunnelstore API
- 2020-09-17 AUTH-3103 CI build fixes
- 2020-09-18 AUTH-3110-use-cfsetup-precache
- 2020-09-17 TUN-3295: Show route command results
- 2020-09-16 TUN-3291: cloudflared tunnel run -h explains how to use flags from parent command
- 2020-09-18 AUTH-3109 upload the checksum to workers kv on github releases
- 2020-09-17 updater service exit code should be 11
- 2020-09-01 TUN-3216: UI improvements
- 2020-08-25 Rebased and passed TunnelEventChan to LogServerInfo in new ReconnectTunnel function
- 2020-08-25 TUN-3321: Add box around logs on UI
- 2020-08-26 TUN-3328: Filter out free tunnel has started log from UI
- 2020-08-27 TUN-3333: Add text to UI explaining how to exit
- 2020-08-27 TUN-3335: Dynamically set connection table size for UI
- 2020-08-10 TUN-3238: Update UI when connection re-connects
- 2020-08-17 TUN-3261: Display connections on UI for free classic tunnels
- 2020-07-24 TUN-3201: Create base cloudflared UI structure
- 2020-07-29 TUN-3200: Add connection information to UI
- 2020-07-24 TUN-3255: Update UI to display URL instead of hostname
- 2020-07-29 TUN-3198: Handle errors while running tunnel UI
2020.9.1
- 2020-09-14 TUN-3395: Unhide named tunnel subcommands, tweak help
- 2020-09-15 TUN-3395: Improve help for list command
- 2020-09-14 TUN-3294: Perform basic validation on arguments of route command; remove default pool name which wasn't valid
- 2020-09-15 TUN-3395: Improve help for list command
- 2020-09-16 Use Go 1.15.2
2020.9.0
- 2020-09-11 TUN-3293: Try to use error information from the body of a failed tunnelstore reresponse if available
- 2020-09-04 AUTH-2653 renabled signing
- 2020-09-04 TUN-3377: Tunnel route should check dns/lb before checking tunnel ID
- 2020-09-04 AUTH-2653 changed to proper file extension
- 2020-09-04 AUTH-2653 handle duplicate key import errors
- 2020-09-04 TUN-3345: tunnel run accepts name of tunnel as argument
- 2020-09-08 AUTH-2653 disble error pipe to see what is failing
- 2020-09-08 AUTH-2653 search for the certificate and not the identity
- 2020-09-09 TUN-3284: Use cloudflared/<version> as user agent of tunnelstore client
- 2020-09-09 TUN-3375: Upgrade x/text and gorilla websocket deps
- 2020-09-09 TUN-3375: Upgrade coredns and prometheus dependencies
- 2020-09-09 AUTH-2653 add notarization to mac build
- 2020-09-08 TUN-3292: Mention cleanup in tunnel run help.
- 2020-08-20 AUTH-2016 fixed variable fail
- 2020-08-12 TUN-3352 extra debug logging for websockets
2020.8.2
- 2020-08-20 AUTH-3021 fixed the git version call by using the older flag
- 2020-08-18 TUN-3268: Each connection has its own event digest to reconnect
2020.8.1
- 2020-08-14 AUTH-2975 don't check /etc on windows
- 2020-08-14 AUTH-2977 log file protection
- 2020-08-18 TUN-3286: Use either ID or name in Named Tunnel subcommands.
- 2020-08-18 AUTH-2712 fixed the mac build script
- 2020-08-19 AUTH-2653 disabling signing until we can get the keys
- 2020-08-05 TUN-3233: List tunnels support filtering by deleted, name, existed at and id
- 2020-08-05 TUN-3237: By default, don't show connections that are pending reconnect
- 2020-08-06 TUN-3242: Build with go 1.14
- 2020-08-07 TUN-3243: Refactor tunnel subcommands to allow commands to compose better
- 2020-08-07 AUTH-2864 - add macos build to github release
- 2020-07-31 AUTH-2857 update homebrew script to use new url
- 2020-07-30 TUN-3213: Create, route and run named tunnels in one command
- 2020-07-07 AUTH-2712 added MSI build for a windows agent
2020.8.0
- 2020-07-30 TUN-3220: tunnel route reports created route
- 2020-07-31 TUN-3221: ConnectionOptions tracks numPreviousAttempts.
- 2020-07-20 TUN-3190: Initialize logger using command line flags in tunnels subcommands
- 2020-07-21 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging
- 2020-07-23 TUN-3194: Don't render log output when level is not enabled
- 2020-07-22 AUTH-2016 adds sha256 hashes to releases
- 2020-07-27 Removes centos 6 build
- 2020-07-27 TUN-3209: Add benchmark for header serialization
- 2020-07-24 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
- 2020-07-26 TUN-3208: Add benchmark for large response write
- 2020-07-27 TUN-3208: Reduce copies and allocations on h2mux write path. Pre-allocate 16KB write buffer on the first write if possible. Use explicit byte array for chunks on write thread to avoid copying through intermediate buffer due to io.CopyN.
- 2020-07-28 AUTH-2714: Adds arm64 cloudflared build
- 2020-07-29 AUTH-2927 run message update after all github builds are done
- 2020-07-17 AUTH-2902 redirect with just the root host on curl commands
2020.7.4
- 2020-07-20 Build cloudflared for arm64 on native agents
- 2020-07-10 TUN-3048: Handle error when user tries to delete active tunnel
- 2020-07-14 AUTH-2890: adds error handler to cli actions
- 2020-07-06 TUN-3156: Add route subcommand under tunnel
2020.7.3
- 2020-07-13 Change scp command to use file glob that matches both cloudflared rpms and debs
+13 -1
View File
@@ -86,6 +86,7 @@ func createWebsocketStream(options *StartOptions, logger logger.Service) (*cfweb
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
defer closeRespBody(resp)
if err != nil && IsAccessResponse(resp) {
wsConn, err = createAccessAuthenticatedStream(options, logger)
if err != nil {
@@ -141,5 +142,16 @@ func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (
dump, err := httputil.DumpRequest(req, false)
logger.Debugf("Access Websocket request: %s", string(dump))
return cfwebsocket.ClientConnect(req, nil)
conn, resp, err := cfwebsocket.ClientConnect(req, nil)
if resp != nil {
r, err := httputil.DumpResponse(resp, true)
if r != nil {
logger.Debugf("Websocket response: %q", r)
} else if err != nil {
logger.Debugf("Websocket response error: %v", err)
}
}
return conn, resp, err
}
+87 -42
View File
@@ -1,4 +1,4 @@
pinned_go: &pinned_go go=1.12.9-1
pinned_go: &pinned_go go=1.15.3-1
build_dir: &build_dir /cfsetup_build
default-flavor: stretch
stretch: &stretch
@@ -22,6 +22,17 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=amd64
- make cloudflared-deb
build-deb-arm64:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
post-cache:
- export GOOS=linux
- export GOARCH=arm64
- make cloudflared-deb
publish-deb:
build_dir: *build_dir
builddeps:
@@ -50,8 +61,9 @@ stretch: &stretch
- build-essential
- python3-setuptools
- python3-pip
post-cache:
pre-cache: &install_pygithub
- pip3 install pygithub
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- make github-release
@@ -74,8 +86,8 @@ stretch: &stretch
- gcc-arm-linux-gnueabihf
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=arm
- export CC=arm-linux-gnueabihf-gcc
@@ -96,8 +108,8 @@ stretch: &stretch
- gcc-multilib
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=386
- make github-release
@@ -118,8 +130,8 @@ stretch: &stretch
- gcc-mingw-w64
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- pip3 install pygithub
- export GOOS=windows
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
@@ -141,12 +153,35 @@ stretch: &stretch
- gcc-mingw-w64
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- pip3 install pygithub
- export GOOS=windows
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
- make github-release
github-release-linux-arm64:
build_dir: *build_dir
builddeps:
- *pinned_go
- crossbuild-essential-armhf
- g++-aarch64-linux-gnu
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- export GOOS=linux
- export GOARCH=arm64
- export CC=aarch64-linux-gnu-gcc
- make github-release
github-release-macos-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- make github-mac-upload
test:
build_dir: *build_dir
builddeps:
@@ -165,50 +200,59 @@ stretch: &stretch
- s3cmd
post-cache:
- .teamcity/update-homebrew.sh
github-message-release:
build_dir: *build_dir
builddeps:
- *pinned_go
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- make github-message
build-junos:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- python3
- genisoimage
- jetez
pre-cache:
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
post-cache:
- export GOOS=freebsd
- export GOARCH=amd64
- make cloudflared-junos
publish-junos:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- python3
- genisoimage
- jetez
- s4cmd
pre-cache:
- ln -s /usr/bin/genisoimage /usr/bin/mkisofs
post-cache:
- export GOOS=freebsd
- export GOARCH=amd64
- make publish-cloudflared-junos
jessie: *stretch
buster: *stretch
centos-6:
build-rpm:
build_dir: *build_dir
builddeps: &el6_builddeps
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
post-cache:
- source /usr/local/rvm/scripts/rvm
- rvm use 1.9
- sudo yum install -y fakeroot
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
- export GOARCH=amd64
- make cloudflared-rpm
publish-rpm:
build_dir: *build_dir
builddeps: *el6_builddeps
post-cache:
- source /usr/local/rvm/scripts/rvm
- rvm use 1.9
- sudo yum install -y fakeroot
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
- export GOARCH=amd64
- make publish-rpm
centos-7:
publish-rpm:
build_dir: *build_dir
builddeps: &el7_builddeps
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
pre-cache:
- yum install -y fakeroot
- wget https://golang.org/dl/go1.15.3.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.3.linux-amd64.tar.gz
post-cache:
- sudo yum install -y fakeroot
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
- export GOARCH=amd64
@@ -216,10 +260,11 @@ centos-7:
build-rpm:
build_dir: *build_dir
builddeps: *el7_builddeps
pre-cache:
- yum install -y fakeroot
- wget https://golang.org/dl/go1.15.3.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.3.linux-amd64.tar.gz
post-cache:
- sudo yum install -y fakeroot
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
- export GOARCH=amd64
+3 -14
View File
@@ -2,7 +2,6 @@ package access
import (
"net/http"
"net/url"
"strings"
"github.com/cloudflare/cloudflared/carrier"
@@ -12,21 +11,16 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
// StartForwarder starts a client side websocket forward
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger logger.Service) error {
validURLString, err := validation.ValidateUrl(forwarder.Listener)
validURL, err := validation.ValidateUrl(forwarder.Listener)
if err != nil {
return errors.Wrap(err, "error validating origin URL")
}
validURL, err := url.Parse(validURLString)
if err != nil {
return errors.Wrap(err, "error parsing origin URL")
}
// get the headers from the config file and add to the request
headers := make(http.Header)
if forwarder.TokenClientID != "" {
@@ -106,12 +100,7 @@ func ssh(c *cli.Context) error {
wsConn := carrier.NewWSConnection(logger, false)
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
localForwarder, err := config.ValidateUrl(c, true)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return errors.Wrap(err, "error validating origin URL")
}
forwarder, err := url.Parse(localForwarder)
forwarder, err := config.ValidateUrl(c, true)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return errors.Wrap(err, "error validating origin URL")
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"golang.org/x/net/idna"
"github.com/getsentry/raven-go"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
const (
+6 -6
View File
@@ -6,7 +6,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
type usageError string
@@ -27,17 +27,17 @@ func UsageError(format string, args ...interface{}) error {
// Ensures exit with error code if actionFunc returns an error
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error {
defer logger.SharedWriteManager.Shutdown()
err := actionFunc(ctx)
if err != nil {
if _, ok := err.(usageError); ok {
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
return cli.Exit(msg, -1)
err = cli.Exit(msg, -1)
} else if _, ok := err.(cli.ExitCoder); !ok {
err = cli.Exit(err.Error(), 1)
}
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
cli.HandleExitCoder(err)
err = cli.Exit(err.Error(), 1)
}
logger.SharedWriteManager.Shutdown()
return err
}
}
+227 -18
View File
@@ -1,16 +1,19 @@
package config
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"time"
homedir "github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
)
@@ -26,10 +29,18 @@ var (
// Launchd doesn't set root env variables, so there is default
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/etc/cloudflared", DefaultUnixConfigLocation}
defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
ErrNoConfigFile = fmt.Errorf("Cannot determine default configuration path. No file %v in %v", DefaultConfigFiles, DefaultConfigSearchDirectories())
)
const DefaultCredentialFile = "cert.pem"
const (
DefaultCredentialFile = "cert.pem"
// BastionFlag is to enable bastion, or jump host, operation
BastionFlag = "bastion"
)
// DefaultConfigDirectory returns the default directory of the config file
func DefaultConfigDirectory() string {
@@ -63,6 +74,16 @@ func DefaultConfigPath() string {
return filepath.Join(dir, DefaultConfigFiles[0])
}
// DefaultConfigSearchDirectories returns the default folder locations of the config
func DefaultConfigSearchDirectories() []string {
dirs := make([]string, len(defaultUserConfigDirs))
copy(dirs, defaultUserConfigDirs)
if runtime.GOOS != "windows" {
dirs = append(dirs, defaultNixConfigDirs...)
}
return dirs
}
// FileExists checks to see if a file exist at the provided path.
func FileExists(path string) (bool, error) {
f, err := os.Open(path)
@@ -77,19 +98,11 @@ func FileExists(path string) (bool, error) {
return true, nil
}
// FindInputSourceContext pulls the input source from the config flag.
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
if context.String("config") != "" {
return altsrc.NewYamlSourceFromFile(context.String("config"))
}
return nil, nil
}
// FindDefaultConfigPath returns the first path that contains a config file.
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
// If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
// contains a config file, return empty string.
func FindDefaultConfigPath() string {
for _, configDir := range DefaultConfigDirs {
for _, configDir := range DefaultConfigSearchDirectories() {
for _, configFile := range DefaultConfigFiles {
dirPath, err := homedir.Expand(configDir)
if err != nil {
@@ -177,14 +190,210 @@ func ValidateUnixSocket(c *cli.Context) (string, error) {
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
var url = c.String("url")
if allowFromArgs && c.NArg() > 0 {
if allowURLFromArgs && c.NArg() > 0 {
if c.IsSet("url") {
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
return nil, errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
}
url = c.Args().Get(0)
}
validUrl, err := validation.ValidateUrl(url)
return validUrl, err
}
type UnvalidatedIngressRule struct {
Hostname string
Path string
Service string
OriginRequest OriginRequestConfig `yaml:"originRequest"`
}
// OriginRequestConfig is a set of optional fields that users may set to
// customize how cloudflared sends requests to origin services. It is used to set
// up general config that apply to all rules, and also, specific per-rule
// config.
// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
type OriginRequestConfig struct {
// HTTP proxy timeout for establishing a new connection
ConnectTimeout *time.Duration `yaml:"connectTimeout"`
// HTTP proxy timeout for completing a TLS handshake
TLSTimeout *time.Duration `yaml:"tlsTimeout"`
// HTTP proxy TCP keepalive duration
TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs *bool `yaml:"noHappyEyeballs"`
// HTTP proxy maximum keepalive connection pool size
KeepAliveConnections *int `yaml:"keepAliveConnections"`
// HTTP proxy timeout for closing an idle connection
KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout"`
// Sets the HTTP Host header for the local webserver.
HTTPHostHeader *string `yaml:"httpHostHeader"`
// Hostname on the origin server certificate.
OriginServerName *string `yaml:"originServerName"`
// 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"`
// Disables TLS verification of the certificate presented by your origin.
// Will allow any certificate from the origin to be accepted.
// Note: The connection from your machine to Cloudflare's Edge is still encrypted.
NoTLSVerify *bool `yaml:"noTLSVerify"`
// Disables chunked transfer encoding.
// Useful if you are running a WSGI server.
DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding"`
// Runs as jump host
BastionMode *bool `yaml:"bastionMode"`
// Listen address for the proxy.
ProxyAddress *string `yaml:"proxyAddress"`
// Listen port for the proxy.
ProxyPort *uint `yaml:"proxyPort"`
// Valid options are 'socks' or empty.
ProxyType *string `yaml:"proxyType"`
}
type Configuration struct {
TunnelID string `yaml:"tunnel"`
Ingress []UnvalidatedIngressRule
OriginRequest OriginRequestConfig `yaml:"originRequest"`
sourceFile string
}
type configFileSettings struct {
Configuration `yaml:",inline"`
// older settings will be aggregated into the generic map, should be read via cli.Context
Settings map[string]interface{} `yaml:",inline"`
}
func (c *Configuration) Source() string {
return c.sourceFile
}
func (c *configFileSettings) Int(name string) (int, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(int); ok {
return v, nil
}
return 0, fmt.Errorf("expected int found %T for %s", raw, name)
}
return 0, nil
}
func (c *configFileSettings) Duration(name string) (time.Duration, error) {
if raw, ok := c.Settings[name]; ok {
switch v := raw.(type) {
case time.Duration:
return v, nil
case string:
return time.ParseDuration(v)
}
return 0, fmt.Errorf("expected duration found %T for %s", raw, name)
}
return 0, nil
}
func (c *configFileSettings) Float64(name string) (float64, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(float64); ok {
return v, nil
}
return 0, fmt.Errorf("expected float found %T for %s", raw, name)
}
return 0, nil
}
func (c *configFileSettings) String(name string) (string, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(string); ok {
return v, nil
}
return "", fmt.Errorf("expected string found %T for %s", raw, name)
}
return "", nil
}
func (c *configFileSettings) StringSlice(name string) ([]string, error) {
if raw, ok := c.Settings[name]; ok {
if slice, ok := raw.([]interface{}); ok {
strSlice := make([]string, len(slice))
for i, v := range slice {
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("expected string, found %T for %v", i, v)
}
strSlice[i] = str
}
return strSlice, nil
}
return nil, fmt.Errorf("expected string slice found %T for %s", raw, name)
}
return nil, nil
}
func (c *configFileSettings) IntSlice(name string) ([]int, error) {
if raw, ok := c.Settings[name]; ok {
if slice, ok := raw.([]interface{}); ok {
intSlice := make([]int, len(slice))
for i, v := range slice {
str, ok := v.(int)
if !ok {
return nil, fmt.Errorf("expected int, found %T for %v ", v, v)
}
intSlice[i] = str
}
return intSlice, nil
}
if v, ok := raw.([]int); ok {
return v, nil
}
return nil, fmt.Errorf("expected int slice found %T for %s", raw, name)
}
return nil, nil
}
func (c *configFileSettings) Generic(name string) (cli.Generic, error) {
return nil, errors.New("option type Generic not supported")
}
func (c *configFileSettings) Bool(name string) (bool, error) {
if raw, ok := c.Settings[name]; ok {
if v, ok := raw.(bool); ok {
return v, nil
}
return false, fmt.Errorf("expected boolean found %T for %s", raw, name)
}
return false, nil
}
var configuration configFileSettings
func GetConfiguration() *Configuration {
return &configuration.Configuration
}
// ReadConfigFile returns InputSourceContext initialized from the configuration file.
// On repeat calls returns with the same file, returns without reading the file again; however,
// if value of "config" flag changes, will read the new config file
func ReadConfigFile(c *cli.Context, log logger.Service) (*configFileSettings, error) {
configFile := c.String("config")
if configuration.Source() == configFile || configFile == "" {
if configuration.Source() == "" {
return nil, ErrNoConfigFile
}
return &configuration, nil
}
log.Debugf("Loading configuration from %s", configFile)
file, err := os.Open(configFile)
if err != nil {
if os.IsNotExist(err) {
err = ErrNoConfigFile
}
return nil, err
}
defer file.Close()
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
return nil, errors.Wrap(err, "error parsing YAML in config file at "+configFile)
}
configuration.sourceFile = configFile
return &configuration, nil
}
@@ -0,0 +1,76 @@
package config
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestConfigFileSettings(t *testing.T) {
var (
firstIngress = UnvalidatedIngressRule{
Hostname: "tunnel1.example.com",
Path: "/id",
Service: "https://localhost:8000",
}
secondIngress = UnvalidatedIngressRule{
Hostname: "*",
Path: "",
Service: "https://localhost:8001",
}
)
rawYAML := `
tunnel: config-file-test
ingress:
- hostname: tunnel1.example.com
path: /id
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
retries: 5
grace-period: 30s
percentage: 3.14
hostname: example.com
tag:
- test
- central-1
counters:
- 123
- 456
`
var config configFileSettings
err := yaml.Unmarshal([]byte(rawYAML), &config)
assert.NoError(t, err)
assert.Equal(t, "config-file-test", config.TunnelID)
assert.Equal(t, firstIngress, config.Ingress[0])
assert.Equal(t, secondIngress, config.Ingress[1])
retries, err := config.Int("retries")
assert.NoError(t, err)
assert.Equal(t, 5, retries)
gracePeriod, err := config.Duration("grace-period")
assert.NoError(t, err)
assert.Equal(t, time.Second*30, gracePeriod)
percentage, err := config.Float64("percentage")
assert.NoError(t, err)
assert.Equal(t, 3.14, percentage)
hostname, err := config.String("hostname")
assert.NoError(t, err)
assert.Equal(t, "example.com", hostname)
tags, err := config.StringSlice("tag")
assert.NoError(t, err)
assert.Equal(t, "test", tags[0])
assert.Equal(t, "central-1", tags[1])
counters, err := config.IntSlice("counters")
assert.NoError(t, err)
assert.Equal(t, 123, counters[0])
assert.Equal(t, 456, counters[1])
}
+2 -2
View File
@@ -1,11 +1,11 @@
package config
import (
"errors"
"os"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/watcher"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
@@ -80,7 +80,7 @@ func readConfigFromPath(configPath string) (Root, error) {
var config Root
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
return Root{}, err
return Root{}, errors.Wrap(err, "error parsing YAML in config file at "+configPath)
}
return config, nil
+2 -1
View File
@@ -4,9 +4,10 @@ import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/watcher"
"github.com/stretchr/testify/assert"
)
type mockNotifier struct {
+1 -1
View File
@@ -5,7 +5,7 @@ package main
import (
"os"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
+65 -20
View File
@@ -7,10 +7,12 @@ import (
"os"
"path/filepath"
"github.com/pkg/errors"
cli "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
@@ -21,12 +23,18 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
&cli.Command{
Name: "install",
Usage: "Install Argo Tunnel as a system service",
Action: installLinuxService,
Action: cliutil.ErrorHandler(installLinuxService),
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "legacy",
Usage: "Generate service file for non-named tunnels",
},
},
},
&cli.Command{
Name: "uninstall",
Usage: "Uninstall the Argo Tunnel service",
Action: uninstallLinuxService,
Action: cliutil.ErrorHandler(uninstallLinuxService),
},
},
})
@@ -39,6 +47,7 @@ const (
serviceConfigDir = "/etc/cloudflared"
serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem"
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
)
var systemdTemplates = []ServiceTemplate{
@@ -51,7 +60,7 @@ After=network.target
[Service]
TimeoutStartSec=0
Type=notify
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --no-autoupdate
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
Restart=on-failure
RestartSec=5s
@@ -66,7 +75,7 @@ Description=Update Argo Tunnel
After=network.target
[Service]
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 64 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
`,
},
{
@@ -101,7 +110,7 @@ var sysvTemplate = ServiceTemplate{
# Description: Argo Tunnel agent
### END INIT INFO
name=$(basename $(readlink -f $0))
cmd="{{.Path}} --config /etc/cloudflared/config.yml --origincert /etc/cloudflared/cert.pem --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s"
cmd="{{.Path}} --config /etc/cloudflared/config.yml --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
pid_file="/var/run/$name.pid"
stdout_log="/var/log/$name.log"
stderr_log="/var/log/$name.err"
@@ -181,10 +190,6 @@ func isSystemd() bool {
}
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, logger logger.Service) error {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
if srcCredentialPath != destCredentialPath {
@@ -215,23 +220,63 @@ func installLinuxService(c *cli.Context) error {
if err != nil {
return fmt.Errorf("error determining executable path: %v", err)
}
templateArgs := ServiceTemplateArgs{Path: etPath}
templateArgs := ServiceTemplateArgs{
Path: etPath,
}
userConfigDir := filepath.Dir(c.String("config"))
userConfigFile := filepath.Base(c.String("config"))
userCredentialFile := config.DefaultCredentialFile
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
if c.Bool("legacy") {
userConfigDir := filepath.Dir(c.String("config"))
userConfigFile := filepath.Base(c.String("config"))
userCredentialFile := config.DefaultCredentialFile
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
return err
}
templateArgs.ExtraArgs = []string{
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
}
} else {
src, err := config.ReadConfigFile(c, logger)
if err != nil {
return err
}
// can't use context because this command doesn't define "credentials-file" flag
configPresent := func(s string) bool {
val, err := src.String(s)
return err == nil && val != ""
}
if src.TunnelID == "" || !configPresent("credentials-file") {
return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
tunnel: TUNNEL-UUID
credentials-file: CREDENTIALS-FILE
`, src.Source())
}
if src.Source() != serviceConfigPath {
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
return fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
}
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
}
}
templateArgs.ExtraArgs = []string{
"tunnel", "run",
}
}
switch {
case isSystemd():
logger.Infof("Using Systemd")
return installSystemd(&templateArgs, logger)
default:
logger.Infof("Using Sysv")
logger.Infof("Using SysV")
return installSysv(&templateArgs, logger)
}
}
@@ -290,7 +335,7 @@ func uninstallLinuxService(c *cli.Context) error {
logger.Infof("Using Systemd")
return uninstallSystemd(logger)
default:
logger.Infof("Using Sysv")
logger.Infof("Using SysV")
return uninstallSysv(logger)
}
}
+4 -3
View File
@@ -6,8 +6,9 @@ import (
"fmt"
"os"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
@@ -24,12 +25,12 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
{
Name: "install",
Usage: "Install Argo Tunnel as an user launch agent",
Action: installLaunchd,
Action: cliutil.ErrorHandler(installLaunchd),
},
{
Name: "uninstall",
Usage: "Uninstall the Argo Tunnel launch agent",
Action: uninstallLaunchd,
Action: cliutil.ErrorHandler(uninstallLaunchd),
},
},
})
+35 -16
View File
@@ -13,11 +13,12 @@ import (
log "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/overwatch"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/watcher"
raven "github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -76,21 +77,42 @@ func main() {
and configure access control.`
app.Flags = flags()
app.Action = action(Version, shutdownC, graceShutdownC)
app.Before = tunnel.Before
app.Before = tunnel.SetFlagsFromConfigFile
app.Commands = commands(cli.ShowVersion)
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
access.Init(shutdownC, graceShutdownC)
updater.Init(Version)
runApp(app, shutdownC, graceShutdownC)
}
func commands(version func(c *cli.Context)) []*cli.Command {
cmds := []*cli.Command{
{
Name: "update",
Action: updater.Update,
Usage: "Update the agent if a new version exists",
ArgsUsage: " ",
Name: "update",
Action: cliutil.ErrorHandler(updater.Update),
Usage: "Update the agent if a new version exists",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "beta",
Usage: "specify if you wish to update to the latest beta version",
},
&cli.BoolFlag{
Name: "force",
Usage: "specify if you wish to force an upgrade to the latest version regardless of the current version",
Hidden: true,
},
&cli.BoolFlag{
Name: "staging",
Usage: "specify if you wish to use the staging url for updating",
Hidden: true,
},
&cli.StringFlag{
Name: "version",
Usage: "specify a version you wish to upgrade or downgrade to",
Hidden: false,
},
},
Description: `Looks for a new version on the official download server.
If a new version exists, updates the agent binary and quits.
Otherwise, does nothing.
@@ -108,6 +130,7 @@ To determine if an update happened in a script, check for error code 64.`,
},
}
cmds = append(cmds, tunnel.Commands()...)
cmds = append(cmds, tunneldns.Command(false))
cmds = append(cmds, access.Commands()...)
return cmds
}
@@ -122,23 +145,19 @@ func isEmptyInvocation(c *cli.Context) bool {
}
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
return func(c *cli.Context) (err error) {
return cliutil.ErrorHandler(func(c *cli.Context) (err error) {
if isEmptyInvocation(c) {
return handleServiceMode(shutdownC)
}
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC, nil) }, nil)
exitCode := 0
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
if err != nil {
handleError(err)
exitCode = 1
captureError(err)
}
// we already handle error printing, so we pass an empty string so we
// don't have to print again.
return cli.Exit("", exitCode)
}
return err
})
}
func userHomeDir() (string, error) {
@@ -154,7 +173,7 @@ func userHomeDir() (string, error) {
}
// In order to keep the amount of noise sent to Sentry low, typical network errors can be filtered out here by a substring match.
func handleError(err error) {
func captureError(err error) {
errorMessage := err.Error()
for _, ignoredErrorMessage := range ignoredErrors {
if strings.Contains(errorMessage, ignoredErrorMessage) {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// GenerateFilePathFromURL will return a filepath for given access application url
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
if err != nil {
return "", err
}
+32 -3
View File
@@ -10,8 +10,9 @@ import (
"os/exec"
"text/template"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/mitchellh/go-homedir"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
)
type ServiceTemplate struct {
@@ -21,7 +22,8 @@ type ServiceTemplate struct {
}
type ServiceTemplateArgs struct {
Path string
Path string
ExtraArgs []string
}
func (st *ServiceTemplate) ResolvePath() (string, error) {
@@ -80,7 +82,7 @@ func runCommand(command string, args ...string) error {
return fmt.Errorf("error starting %s: %v", command, err)
}
ioutil.ReadAll(stderr)
_, _ = ioutil.ReadAll(stderr)
err = cmd.Wait()
if err != nil {
return fmt.Errorf("%s returned with error: %v", command, err)
@@ -139,6 +141,33 @@ func copyCredential(srcCredentialPath, destCredentialPath string) error {
return nil
}
func copyFile(src, dest string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(dest)
if err != nil {
return err
}
ok := false
defer func() {
destFile.Close()
if !ok {
_ = os.Remove(dest)
}
}()
if _, err := io.Copy(destFile, srcFile); err != nil {
return err
}
ok = true
return nil
}
func copyConfig(srcConfigPath, destConfigPath string) error {
// Copy or create config
destFile, exists, err := openFile(destConfigPath, true)
+9 -9
View File
@@ -128,19 +128,19 @@ func isTokenLocked(lockFilePath string) bool {
}
// FetchTokenWithRedirect will either load a stored token or generate a new one
// it appends a redirect URL to the access cli request if opening the browser
// it appends the full url as the redirect URL to the access cli request if opening the browser
func FetchTokenWithRedirect(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, true, logger)
}
// FetchToken will either load a stored token or generate a new one
// it doesn't append a redirect URL to the access cli request if opening the browser
func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, false, logger)
}
// FetchToken will either load a stored token or generate a new one
// it appends the host of the appURL as the redirect URL to the access cli request if opening the browser
func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, true, logger)
}
// getToken will either load a stored token or generate a new one
func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (string, error) {
func getToken(appURL *url.URL, useHostOnly bool, logger logger.Service) (string, error) {
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
return token, nil
}
@@ -166,7 +166,7 @@ func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (stri
// this weird parameter is the resource name (token) and the key/value
// we want to send to the transfer service. the key is token and the value
// is blank (basically just the id generated in the transfer service)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, shouldRedirect, logger)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, useHostOnly, logger)
if err != nil {
return "", err
}
+8 -8
View File
@@ -28,12 +28,12 @@ const (
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
// the user to complete an action, while it long polls in the background waiting for an
// action to be completed to download the resource.
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, shouldRedirect bool, logger logger.Service) ([]byte, error) {
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, useHostOnly bool, logger logger.Service) ([]byte, error) {
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil {
return nil, err
}
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, shouldRedirect)
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly)
if err != nil {
return nil, err
}
@@ -82,18 +82,18 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
// BuildRequestURL creates a request suitable for a resource transfer.
// it will return a constructed url based off the base url and query key/value provided.
// cli will build a url for cli transfer request.
func buildRequestURL(baseURL *url.URL, key, value string, cli, shouldRedirect bool) (string, error) {
func buildRequestURL(baseURL *url.URL, key, value string, cli, useHostOnly bool) (string, error) {
q := baseURL.Query()
q.Set(key, value)
baseURL.RawQuery = q.Encode()
if useHostOnly {
baseURL.Path = ""
}
if !cli {
return baseURL.String(), nil
}
if shouldRedirect {
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
}
baseURL.RawQuery = q.Encode() // and this actual baseURL.
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
baseURL.RawQuery = q.Encode() // and this actual baseURL.
baseURL.Path = "cdn-cgi/access/cli"
return baseURL.String(), nil
}
File diff suppressed because it is too large Load Diff
+100 -115
View File
@@ -1,19 +1,20 @@
package tunnel
import (
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig"
@@ -23,8 +24,8 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
)
var (
@@ -34,10 +35,10 @@ var (
argumentsUrl = developerPortal + "/reference/arguments/"
)
// returns the first path that contains a cert.pem file. If none of the DefaultConfigDirs
// returns the first path that contains a cert.pem file. If none of the DefaultConfigSearchDirectories
// contains a cert.pem file, return empty string
func findDefaultOriginCertPath() string {
for _, defaultConfigDir := range config.DefaultConfigDirs {
for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() {
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
if ok, _ := config.FileExists(originCertPath); ok {
return originCertPath
@@ -95,7 +96,7 @@ func dnsProxyStandAlone(c *cli.Context) bool {
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
originCertPath := c.String("origincert")
if originCertPath == "" {
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigSearchDirectories())
if isRunningFromTerminal() {
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
return "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
@@ -158,109 +159,49 @@ func prepareTunnelConfig(
version string,
logger logger.Service,
transportLogger logger.Service,
namedTunnel *origin.NamedTunnelConfig,
) (*origin.TunnelConfig, error) {
compatibilityMode := namedTunnel == nil
namedTunnel *connection.NamedTunnelConfig,
uiIsEnabled bool,
) (*origin.TunnelConfig, ingress.Ingress, error) {
isNamedTunnel := namedTunnel != nil
hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil {
logger.Errorf("Invalid hostname: %s", err)
return nil, errors.Wrap(err, "Invalid hostname")
return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname")
}
isFreeTunnel := hostname == ""
clientID := c.String("id")
if !c.IsSet("id") {
clientID, err = generateRandomClientID(logger)
if err != nil {
return nil, err
return nil, ingress.Ingress{}, err
}
}
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
if err != nil {
logger.Errorf("Tag parse failure: %s", err)
return nil, errors.Wrap(err, "Tag parse failure")
return nil, ingress.Ingress{}, errors.Wrap(err, "Tag parse failure")
}
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
originURL, err := config.ValidateUrl(c, compatibilityMode)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return nil, errors.Wrap(err, "Error validating origin URL")
}
var originCert []byte
if !isFreeTunnel {
originCert, err = getOriginCert(c, logger)
if err != nil {
return nil, errors.Wrap(err, "Error getting origin cert")
return nil, ingress.Ingress{}, errors.Wrap(err, "Error getting origin cert")
}
}
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
if err != nil {
logger.Errorf("Error loading cert pool: %s", err)
return nil, errors.Wrap(err, "Error loading cert pool")
}
tunnelMetrics := origin.NewTunnelMetrics()
httpTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: c.Int("proxy-keepalive-connections"),
MaxIdleConnsPerHost: c.Int("proxy-keepalive-connections"),
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
}
dialer := &net.Dialer{
Timeout: c.Duration("proxy-connect-timeout"),
KeepAlive: c.Duration("proxy-tcp-keepalive"),
}
if c.Bool("proxy-no-happy-eyeballs") {
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
}
dialContext := dialer.DialContext
if c.IsSet("unix-socket") {
unixSocket, err := config.ValidateUnixSocket(c)
if err != nil {
logger.Errorf("Error validating --unix-socket: %s", err)
return nil, errors.Wrap(err, "Error validating --unix-socket")
}
logger.Infof("Proxying tunnel requests to unix:%s", unixSocket)
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
// if --unix-socket specified, enforce network type "unix"
return dialContext(ctx, "unix", unixSocket)
}
} else {
logger.Infof("Proxying tunnel requests to %s", originURL)
httpTransport.DialContext = dialContext
}
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
}
// If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client.
if !c.IsSet(bastionFlag) {
if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil {
logger.Errorf("unable to connect to the origin: %s", err)
}
}
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
if err != nil {
logger.Errorf("unable to create TLS config to connect with edge: %s", err)
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
if namedTunnel != nil {
var (
ingressRules ingress.Ingress
classicTunnel *connection.ClassicTunnelConfig
)
if isNamedTunnel {
clientUUID, err := uuid.NewRandom()
if err != nil {
return nil, errors.Wrap(err, "can't generate clientUUID")
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate clientUUID")
}
namedTunnel.Client = tunnelpogs.ClientInfo{
ClientID: clientUUID[:],
@@ -268,42 +209,86 @@ func prepareTunnelConfig(
Version: version,
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
}
ingressRules, err = ingress.ParseIngress(config.GetConfiguration())
if err != nil && err != ingress.ErrNoIngressRules {
return nil, ingress.Ingress{}, err
}
if !ingressRules.IsEmpty() && c.IsSet("url") {
return nil, ingress.Ingress{}, ingress.ErrURLIncompatibleWithIngress
}
} else {
classicTunnel = &connection.ClassicTunnelConfig{
Hostname: hostname,
OriginCert: originCert,
// turn off use of reconnect token and auth refresh when using named tunnels
UseReconnectToken: !isNamedTunnel && c.Bool("use-reconnect-token"),
}
}
// Convert single-origin configuration into multi-origin configuration.
if ingressRules.IsEmpty() {
ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel, logger)
if err != nil {
return nil, ingress.Ingress{}, err
}
}
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), namedTunnel, edgediscovery.HTTP2Percentage, origin.ResolveTTL, logger)
if err != nil {
return nil, ingress.Ingress{}, err
}
logger.Infof("Initial protocol %s", protocolSelector.Current())
edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList))
for _, p := range connection.ProtocolList {
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, p.ServerName())
if err != nil {
return nil, ingress.Ingress{}, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
edgeTLSConfigs[p] = edgeTLSConfig
}
originClient := origin.NewClient(ingressRules, tags, logger)
connectionConfig := &connection.Config{
OriginClient: originClient,
GracePeriod: c.Duration("grace-period"),
ReplaceExisting: c.Bool("force"),
}
muxerConfig := &connection.MuxerConfig{
HeartbeatInterval: c.Duration("heartbeat-interval"),
MaxHeartbeats: c.Uint64("heartbeat-count"),
CompressionSetting: h2mux.CompressionSetting(c.Uint64("compression-quality")),
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
}
var tunnelEventChan chan ui.TunnelEvent
if uiIsEnabled {
tunnelEventChan = make(chan ui.TunnelEvent, 16)
}
return &origin.TunnelConfig{
BuildInfo: buildInfo,
ClientID: clientID,
ClientTlsConfig: httpTransport.TLSClientConfig,
CompressionQuality: c.Uint64("compression-quality"),
EdgeAddrs: c.StringSlice("edge"),
GracePeriod: c.Duration("grace-period"),
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
HeartbeatInterval: c.Duration("heartbeat-interval"),
Hostname: hostname,
HTTPHostHeader: c.String("http-host-header"),
IncidentLookup: origin.NewIncidentLookup(),
IsAutoupdated: c.Bool("is-autoupdated"),
IsFreeTunnel: isFreeTunnel,
LBPool: c.String("lb-pool"),
Logger: logger,
TransportLogger: transportLogger,
MaxHeartbeats: c.Uint64("heartbeat-count"),
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
OriginCert: originCert,
OriginUrl: originURL,
ReportedVersion: version,
Retries: c.Uint("retries"),
RunFromTerminal: isRunningFromTerminal(),
Tags: tags,
TlsConfig: toEdgeTLSConfig,
NamedTunnel: namedTunnel,
ReplaceExisting: c.Bool("force"),
// turn off use of reconnect token and auth refresh when using named tunnels
UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"),
}, nil
ConnectionConfig: connectionConfig,
BuildInfo: buildInfo,
ClientID: clientID,
EdgeAddrs: c.StringSlice("edge"),
HAConnections: c.Int("ha-connections"),
IncidentLookup: origin.NewIncidentLookup(),
IsAutoupdated: c.Bool("is-autoupdated"),
IsFreeTunnel: isFreeTunnel,
LBPool: c.String("lb-pool"),
Tags: tags,
Logger: logger,
Observer: connection.NewObserver(transportLogger, tunnelEventChan),
ReportedVersion: version,
Retries: c.Uint("retries"),
RunFromTerminal: isRunningFromTerminal(),
NamedTunnel: namedTunnel,
ClassicTunnel: classicTunnel,
MuxerConfig: muxerConfig,
TunnelEventChan: tunnelEventChan,
ProtocolSelector: protocolSelector,
EdgeTLSConfigs: edgeTLSConfigs,
}, ingressRules, nil
}
func isRunningFromTerminal() bool {
@@ -0,0 +1,114 @@
package tunnel
import (
"fmt"
"net/url"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/ingress"
)
func buildIngressSubcommand() *cli.Command {
return &cli.Command{
Name: "ingress",
Category: "Tunnel",
Usage: "Validate and test cloudflared tunnel's ingress configuration",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress COMMAND [arguments...]",
Hidden: true,
Description: ` Cloudflared lets you route traffic from the internet to multiple different addresses on your
origin. Multiple-origin routing is configured by a set of rules. Each rule matches traffic
by its hostname or path, and routes it to an address. These rules are configured under the
'ingress' key of your config.yaml, for example:
ingress:
- hostname: www.example.com
service: https://localhost:8000
- hostname: *.example.xyz
path: /[a-zA-Z]+.html
service: https://localhost:8001
- hostname: *
service: https://localhost:8002
To ensure cloudflared can route all incoming requests, the last rule must be a catch-all
rule that matches all traffic. You can validate these rules with the 'ingress validate'
command, and test which rule matches a particular URL with 'ingress rule <URL>'.
Multiple-origin routing is incompatible with the --url flag.`,
Subcommands: []*cli.Command{buildValidateIngressCommand(), buildTestURLCommand()},
}
}
func buildValidateIngressCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ErrorHandler(validateIngressCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
}
}
func buildTestURLCommand() *cli.Command {
return &cli.Command{
Name: "rule",
Action: cliutil.ErrorHandler(testURLCommand),
Usage: "Check which ingress rule matches a given request URL",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL",
ArgsUsage: "URL",
Description: "Check which ingress rule matches a given request URL. " +
"Ingress rules match a request's hostname and path. Hostname is " +
"optional and is either a full hostname like `www.example.com` or a " +
"hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " +
"is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`",
}
}
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
func validateIngressCommand(c *cli.Context) error {
conf := config.GetConfiguration()
if conf.Source() == "" {
fmt.Println("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
return nil
}
fmt.Println("Validating rules from", conf.Source())
if _, err := ingress.ParseIngress(conf); err != nil {
return errors.Wrap(err, "Validation failed")
}
if c.IsSet("url") {
return ingress.ErrURLIncompatibleWithIngress
}
fmt.Println("OK")
return nil
}
// testURLCommand checks which ingress rule matches the given URL.
func testURLCommand(c *cli.Context) error {
requestArg := c.Args().First()
if requestArg == "" {
return errors.New("cloudflared tunnel rule expects a single argument, the URL to test")
}
requestURL, err := url.Parse(requestArg)
if err != nil {
return fmt.Errorf("%s is not a valid URL", requestArg)
}
if requestURL.Hostname() == "" && requestURL.Scheme == "" {
return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg)
}
conf := config.GetConfiguration()
fmt.Println("Using rules from", conf.Source())
ing, err := ingress.ParseIngress(conf)
if err != nil {
return errors.Wrap(err, "Validation failed")
}
_, i := ing.FindMatchingRule(requestURL.Hostname(), requestURL.Path)
fmt.Printf("Matched rule #%d\n", i+1)
fmt.Println(ing.Rules[i].MultiLineString())
return nil
}
+23 -5
View File
@@ -7,12 +7,14 @@ import (
"path/filepath"
"syscall"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/logger"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
const (
@@ -20,6 +22,22 @@ const (
callbackStoreURL = "https://login.argotunnel.com/"
)
func buildLoginSubcommand(hidden bool) *cli.Command {
return &cli.Command{
Name: "login",
Action: cliutil.ErrorHandler(login),
Usage: "Generate a configuration file with your login details",
ArgsUsage: " ",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "url",
Hidden: true,
},
},
Hidden: hidden,
}
}
func login(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
@@ -40,7 +58,7 @@ func login(c *cli.Context) error {
return err
}
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, logger)
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, false, logger)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
return err
@@ -51,7 +69,7 @@ func login(c *cli.Context) error {
}
func checkForExistingCert() (string, bool, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
if err != nil {
return "", false, err
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunneldns"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -0,0 +1,374 @@
package tunnel
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
type errInvalidJSONCredential struct {
err error
path string
}
func (e errInvalidJSONCredential) Error() string {
return "Invalid JSON when parsing tunnel credentials file"
}
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to
// pass between subcommands, and make sure they are only initialized once
type subcommandContext struct {
c *cli.Context
logger logger.Service
// These fields should be accessed using their respective Getter
tunnelstoreClient tunnelstore.Client
userCredential *userCredential
isUIEnabled bool
}
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
isUIEnabled := c.IsSet(uiFlag) && c.String("name") != ""
// If UI is enabled, terminal log output should be disabled -- log should be written into a UI log window instead
logger, err := createLogger(c, false, isUIEnabled)
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
return &subcommandContext{
c: c,
logger: logger,
isUIEnabled: isUIEnabled,
}, nil
}
type userCredential struct {
cert *certutil.OriginCert
certPath string
}
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
if sc.tunnelstoreClient != nil {
return sc.tunnelstoreClient, nil
}
credential, err := sc.credential()
if err != nil {
return nil, err
}
userAgent := fmt.Sprintf("cloudflared/%s", version)
client, err := tunnelstore.NewRESTClient(sc.c.String("api-url"), credential.cert.AccountID, credential.cert.ZoneID, credential.cert.ServiceKey, userAgent, sc.logger)
if err != nil {
return nil, err
}
sc.tunnelstoreClient = client
return client, nil
}
func (sc *subcommandContext) credential() (*userCredential, error) {
if sc.userCredential == nil {
originCertPath, err := findOriginCert(sc.c, sc.logger)
if err != nil {
return nil, errors.Wrap(err, "Error locating origin cert")
}
blocks, err := readOriginCert(originCertPath, sc.logger)
if err != nil {
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
}
cert, err := certutil.DecodeOriginCert(blocks)
if err != nil {
return nil, errors.Wrap(err, "Error decoding origin cert")
}
if cert.AccountID == "" {
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
}
sc.userCredential = &userCredential{
cert: cert,
certPath: originCertPath,
}
}
return sc.userCredential, nil
}
func (sc *subcommandContext) readTunnelCredentials(tunnelID uuid.UUID) (*pogs.TunnelAuth, error) {
filePath, err := sc.tunnelCredentialsPath(tunnelID)
if err != nil {
return nil, err
}
body, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
}
var auth pogs.TunnelAuth
if err = json.Unmarshal(body, &auth); err != nil {
if strings.HasSuffix(filePath, ".pem") {
return nil, fmt.Errorf("The tunnel credentials file should be .json but you gave a .pem. "+
"The tunnel credentials file was originally created by `cloudflared tunnel create` and named %s.json."+
"You may have accidentally used the filepath to cert.pem, which is generated by `cloudflared tunnel "+
"login`.", tunnelID)
}
return nil, errInvalidJSONCredential{path: filePath, err: err}
}
return &auth, nil
}
func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, error) {
if filePath := sc.c.String("credentials-file"); filePath != "" {
if validFilePath(filePath) {
return filePath, nil
}
return "", fmt.Errorf("Tunnel credentials file %s doesn't exist or is not a file", filePath)
}
// Fallback to look for tunnel credentials in the origin cert directory
if originCertPath, err := findOriginCert(sc.c, sc.logger); err == nil {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
// Last resort look under default config directories
for _, configDir := range config.DefaultConfigSearchDirectories() {
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
}
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
client, err := sc.client()
if err != nil {
return nil, errors.Wrap(err, "couldn't create client to talk to Argo Tunnel backend")
}
tunnelSecret, err := generateTunnelSecret()
if err != nil {
return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel")
}
tunnel, err := client.CreateTunnel(name, tunnelSecret)
if err != nil {
return nil, errors.Wrap(err, "Create Tunnel API call failed")
}
credential, err := sc.credential()
if err != nil {
return nil, err
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, credential.cert.AccountID, credential.certPath, tunnelSecret, sc.logger); err != nil {
var errorLines []string
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
if deleteErr := client.DeleteTunnel(tunnel.ID); 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 {
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
}
errorMsg := strings.Join(errorLines, "\n")
return nil, errors.New(errorMsg)
}
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
return nil, renderOutput(outputFormat, &tunnel)
}
sc.logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
return tunnel, nil
}
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
client, err := sc.client()
if err != nil {
return nil, err
}
return client.ListTunnels(filter)
}
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
forceFlagSet := sc.c.Bool("force")
client, err := sc.client()
if err != nil {
return err
}
for _, id := range tunnelIDs {
tunnel, err := client.GetTunnel(id)
if err != nil {
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", tunnel.ID)
}
// Check if tunnel DeletedAt field has already been set
if !tunnel.DeletedAt.IsZero() {
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
}
// Check if tunnel has existing connections and if force flag is set, cleanup connections
if len(tunnel.Connections) > 0 {
if !forceFlagSet {
return fmt.Errorf("You can not delete tunnel %s because it has active connections. To see connections run the 'list' command. If you believe the tunnel is not active, you can use a -f / --force flag with this command.", id)
}
if err := client.CleanupConnections(tunnel.ID); err != nil {
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
}
}
if err := client.DeleteTunnel(tunnel.ID); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
}
tunnelCredentialsPath, err := sc.tunnelCredentialsPath(tunnel.ID)
if err != nil {
sc.logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
return nil
}
if err = os.Remove(tunnelCredentialsPath); err != nil {
sc.logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
}
}
return nil
}
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
credentials, err := sc.readTunnelCredentials(tunnelID)
if err != nil {
if e, ok := err.(errInvalidJSONCredential); ok {
sc.logger.Errorf("The credentials file at %s contained invalid JSON. This is probably caused by passing the wrong filepath. Reminder: the credentials file is a .json file created via `cloudflared tunnel create`.", e.path)
sc.logger.Errorf("Invalid JSON when parsing credentials file: %s", e.err.Error())
}
return err
}
return StartServer(
sc.c,
version,
shutdownC,
graceShutdownC,
&connection.NamedTunnelConfig{Auth: *credentials, ID: tunnelID},
sc.logger,
sc.isUIEnabled,
)
}
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
client, err := sc.client()
if err != nil {
return err
}
for _, tunnelID := range tunnelIDs {
sc.logger.Infof("Cleanup connection for tunnel %s", tunnelID)
if err := client.CleanupConnections(tunnelID); err != nil {
sc.logger.Errorf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
}
}
return nil
}
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
client, err := sc.client()
if err != nil {
return nil, err
}
return client.RouteTunnel(tunnelID, r)
}
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
filter := tunnelstore.NewFilter()
filter.NoDeleted()
filter.ByName(name)
tunnels, err := sc.list(filter)
if err != nil {
return nil, false, err
}
if len(tunnels) == 0 {
return nil, false, nil
}
// There should only be 1 active tunnel for a given name
return tunnels[0], true, nil
}
// findID parses the input. If it's a UUID, return the UUID.
// Otherwise, assume it's a name, and look up the ID of that tunnel.
func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
if u, err := uuid.Parse(input); err == nil {
return u, nil
}
if tunnel, found, err := sc.tunnelActive(input); err != nil {
return uuid.Nil, err
} else if found {
return tunnel.ID, nil
}
return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input)
}
// findIDs is just like mapping `findID` over a slice, but it only uses
// one Tunnelstore API call.
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
// First, look up all tunnels the user has
filter := tunnelstore.NewFilter()
filter.NoDeleted()
tunnels, err := sc.list(filter)
if err != nil {
return nil, err
}
// Do the pure list-processing in its own function, so that it can be
// unit tested easily.
return findIDs(tunnels, inputs)
}
func findIDs(tunnels []*tunnelstore.Tunnel, inputs []string) ([]uuid.UUID, error) {
// Put them into a dictionary for faster lookups
nameToID := make(map[string]uuid.UUID, len(tunnels))
for _, tunnel := range tunnels {
nameToID[tunnel.Name] = tunnel.ID
}
// For each input, try to find the tunnel ID.
tunnelIDs := make([]uuid.UUID, len(inputs))
var badInputs []string
for i, input := range inputs {
if id, err := uuid.Parse(input); err == nil {
tunnelIDs[i] = id
} else if id, ok := nameToID[input]; ok {
tunnelIDs[i] = id
} else {
badInputs = append(badInputs, input)
}
}
if len(badInputs) > 0 {
msg := "Please specify either the ID or name of a tunnel. The following inputs were neither: %s"
return nil, fmt.Errorf(msg, strings.Join(badInputs, ", "))
}
return tunnelIDs, nil
}
@@ -0,0 +1,82 @@
package tunnel
import (
"reflect"
"testing"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
)
func Test_findIDs(t *testing.T) {
type args struct {
tunnels []*tunnelstore.Tunnel
inputs []string
}
tests := []struct {
name string
args args
want []uuid.UUID
wantErr bool
}{
{
name: "input not found",
args: args{
inputs: []string{"asdf"},
},
wantErr: true,
},
{
name: "only UUID",
args: args{
inputs: []string{"a8398a0b-876d-48ed-b609-3fcfd67a4950"},
},
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
},
{
name: "only name",
args: args{
tunnels: []*tunnelstore.Tunnel{
{
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
Name: "tunnel1",
},
},
inputs: []string{"tunnel1"},
},
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
},
{
name: "both UUID and name",
args: args{
tunnels: []*tunnelstore.Tunnel{
{
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
Name: "tunnel1",
},
{
ID: uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
Name: "tunnel2",
},
},
inputs: []string{"tunnel1", "bf028b68-744f-466e-97f8-c46161d80aa5"},
},
want: []uuid.UUID{
uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := findIDs(tt.args.tunnels, tt.args.inputs)
if (err != nil) != tt.wantErr {
t.Errorf("findIDs() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("findIDs() = %v, want %v", got, tt.want)
}
})
}
}
+325 -231
View File
@@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"text/tabwriter"
@@ -15,14 +16,15 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"golang.org/x/net/idna"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
@@ -37,36 +39,77 @@ var (
Aliases: []string{"d"},
Usage: "Include deleted tunnels in the list",
}
outputFormatFlag = &cli.StringFlag{
listNameFlag = &cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "List tunnels with the given `NAME`",
}
listExistedAtFlag = &cli.TimestampFlag{
Name: "when",
Aliases: []string{"w"},
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
Layout: tunnelstore.TimeLayout,
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
}
listIDFlag = &cli.StringFlag{
Name: "id",
Aliases: []string{"i"},
Usage: "List tunnel by `ID`",
}
showRecentlyDisconnected = &cli.BoolFlag{
Name: "show-recently-disconnected",
Aliases: []string{"rd"},
Usage: "Include connections that have recently disconnected in the list",
}
outputFormatFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
}
forceFlag = &cli.BoolFlag{
})
forceFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "By default, if a tunnel is currently being run from a cloudflared, you can't " +
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
"tunnels, you can do so with Cloudflare's Load Balancer product.",
}
credentialsFileFlag = &cli.StringFlag{
})
credentialsFileFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: "credentials-file",
Aliases: []string{credFileFlagAlias},
Usage: "File path of tunnel credentials",
EnvVars: []string{"TUNNEL_CRED_FILE"},
})
forceDeleteFlag = &cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Allows you to delete a tunnel, even if it has active connections.",
EnvVars: []string{"TUNNEL_RUN_FORCE_OVERWRITE"},
}
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: "protocol",
Value: "h2mux",
Aliases: []string{"p"},
Usage: fmt.Sprintf("Protocol implementation to connect with Cloudflare's edge network. %s", connection.AvailableProtocolFlagMessage),
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
Hidden: true,
})
)
const hideSubcommands = true
func buildCreateCommand() *cli.Command {
return &cli.Command{
Name: "create",
Action: cliutil.ErrorHandler(createTunnel),
Action: cliutil.ErrorHandler(createCommand),
Usage: "Create a new tunnel with given name",
ArgsUsage: "TUNNEL-NAME",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag},
UsageText: "cloudflared tunnel [tunnel command options] create [subcommand options] NAME",
Description: `Creates a tunnel, registers it with Cloudflare edge and generates credential file used to run this tunnel.
Use "cloudflared tunnel route" subcommand to map a DNS name to this tunnel and "cloudflared tunnel run" to start the connection.
For example, to create a tunnel named 'my-tunnel' run:
$ cloudflared tunnel create my-tunnel`,
Flags: []cli.Flag{outputFormatFlag},
CustomHelpTemplate: commandHelpTemplate(),
}
}
@@ -77,62 +120,28 @@ func generateTunnelSecret() ([]byte, error) {
return randomBytes, err
}
func createTunnel(c *cli.Context) error {
func createCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create.`)
}
name := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
tunnelSecret, err := generateTunnelSecret()
if err != nil {
return err
}
cert, originCertPath, err := getOriginCertFromContext(c, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
tunnel, err := client.CreateTunnel(name, tunnelSecret)
if err != nil {
return errors.Wrap(err, "Error creating a new tunnel")
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, cert.AccountID, originCertPath, tunnelSecret, logger); err != nil {
var errorLines []string
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
if deleteErr := client.DeleteTunnel(tunnel.ID); 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 {
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
}
errorMsg := strings.Join(errorLines, "\n")
return errors.New(errorMsg)
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, &tunnel)
}
logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
return nil
_, err = sc.create(name)
return errors.Wrap(err, "failed to create tunnel")
}
func tunnelFilePath(tunnelID, directory string) (string, error) {
func tunnelFilePath(tunnelID uuid.UUID, directory string) (string, error) {
fileName := fmt.Sprintf("%v.json", tunnelID)
filePath := filepath.Clean(fmt.Sprintf("%s/%s", directory, fileName))
return homedir.Expand(filePath)
}
func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
func writeTunnelCredentials(tunnelID uuid.UUID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
originCertDir := filepath.Dir(originCertPath)
filePath, err := tunnelFilePath(tunnelID, originCertDir)
if err != nil {
@@ -150,51 +159,6 @@ func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSe
return ioutil.WriteFile(filePath, body, 400)
}
func readTunnelCredentials(c *cli.Context, tunnelID string, logger logger.Service) (*pogs.TunnelAuth, error) {
filePath, err := tunnelCredentialsPath(c, tunnelID, logger)
if err != nil {
return nil, err
}
body, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
}
var auth pogs.TunnelAuth
if err = json.Unmarshal(body, &auth); err != nil {
return nil, err
}
return &auth, nil
}
func tunnelCredentialsPath(c *cli.Context, tunnelID string, logger logger.Service) (string, error) {
if filePath := c.String("credentials-file"); filePath != "" {
if validFilePath(filePath) {
return filePath, nil
}
}
// Fallback to look for tunnel credentials in the origin cert directory
if originCertPath, err := findOriginCert(c, logger); err == nil {
originCertDir := filepath.Dir(originCertPath)
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
// Last resort look under default config directories
for _, configDir := range config.DefaultConfigDirs {
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
}
func validFilePath(path string) bool {
fileStat, err := os.Stat(path)
if err != nil {
@@ -205,41 +169,43 @@ func validFilePath(path string) bool {
func buildListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listTunnels),
Usage: "List existing tunnels",
ArgsUsage: " ",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag},
Name: "list",
Action: cliutil.ErrorHandler(listCommand),
Usage: "List existing tunnels",
UsageText: "cloudflared tunnel [tunnel command options] list [subcommand options]",
Description: "cloudflared tunnel list will display all active tunnels, their created time and associated connections. Use -d flag to include deleted tunnels. See the list of options to filter the list",
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag, listNameFlag, listExistedAtFlag, listIDFlag, showRecentlyDisconnected},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func listTunnels(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
cert, _, err := getOriginCertFromContext(c, logger)
func listCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
allTunnels, err := client.ListTunnels()
if err != nil {
return errors.Wrap(err, "Error listing tunnels")
filter := tunnelstore.NewFilter()
if !c.Bool("show-deleted") {
filter.NoDeleted()
}
if name := c.String("name"); name != "" {
filter.ByName(name)
}
if existedAt := c.Timestamp("time"); existedAt != nil {
filter.ByExistedAt(*existedAt)
}
if id := c.String("id"); id != "" {
tunnelID, err := uuid.Parse(id)
if err != nil {
return errors.Wrapf(err, "%s is not a valid tunnel ID", id)
}
filter.ByTunnelID(tunnelID)
}
var tunnels []tunnelstore.Tunnel
if c.Bool("show-deleted") {
tunnels = allTunnels
} else {
for _, tunnel := range allTunnels {
if tunnel.DeletedAt.IsZero() {
tunnels = append(tunnels, tunnel)
}
}
tunnels, err := sc.list(filter)
if err != nil {
return err
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
@@ -247,15 +213,14 @@ func listTunnels(c *cli.Context) error {
}
if len(tunnels) > 0 {
fmtAndPrintTunnelList(tunnels)
fmtAndPrintTunnelList(tunnels, c.Bool("show-recently-disconnected"))
} else {
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
}
return nil
}
func fmtAndPrintTunnelList(tunnels []tunnelstore.Tunnel) {
func fmtAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
const (
minWidth = 0
tabWidth = 8
@@ -265,26 +230,32 @@ func fmtAndPrintTunnelList(tunnels []tunnelstore.Tunnel) {
)
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
defer writer.Flush()
// Print column headers with tabbed columns
fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t")
// Loop through tunnels, create formatted string for each, and print using tabwriter
for _, t := range tunnels {
formattedStr := fmt.Sprintf("%s\t%s\t%s\t%s\t", t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections))
formattedStr := fmt.Sprintf(
"%s\t%s\t%s\t%s\t",
t.ID,
t.Name,
t.CreatedAt.Format(time.RFC3339),
fmtConnections(t.Connections, showRecentlyDisconnected),
)
fmt.Fprintln(writer, formattedStr)
}
// Write data buffered in tabwriter to output
writer.Flush()
}
func fmtConnections(connections []tunnelstore.Connection) string {
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
// Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections))
for _, connection := range connections {
numConnsPerColo[connection.ColoName]++
if !connection.IsPendingReconnect || showRecentlyDisconnected {
numConnsPerColo[connection.ColoName]++
}
}
// Get sorted list of colos
@@ -304,47 +275,32 @@ func fmtConnections(connections []tunnelstore.Connection) string {
func buildDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Action: cliutil.ErrorHandler(deleteTunnel),
Usage: "Delete existing tunnel with given ID",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
Flags: []cli.Flag{credentialsFileFlag},
Name: "delete",
Action: cliutil.ErrorHandler(deleteCommand),
Usage: "Delete existing tunnel by UUID or name",
UsageText: "cloudflared tunnel [tunnel command options] delete [subcommand options] TUNNEL",
Description: "cloudflared tunnel delete will delete tunnels with the given tunnel UUIDs or names. A tunnel cannot be deleted if it has active connections. To delete the tunnel unconditionally, use -f flag.",
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func deleteTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete.`)
}
id := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
cert, _, err := getOriginCertFromContext(c, logger)
func deleteCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
if err := client.DeleteTunnel(id); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", id)
if c.NArg() < 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`)
}
tunnelCredentialsPath, err := tunnelCredentialsPath(c, id, logger)
tunnelIDs, err := sc.findIDs(c.Args().Slice())
if err != nil {
logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
return nil
return err
}
if err = os.Remove(tunnelCredentialsPath); err != nil {
logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
}
return nil
return sc.delete(tunnelIDs)
}
func renderOutput(format string, v interface{}) error {
@@ -360,99 +316,237 @@ func renderOutput(format string, v interface{}) error {
}
}
func newTunnelstoreClient(c *cli.Context, cert *certutil.OriginCert, logger logger.Service) tunnelstore.Client {
client := tunnelstore.NewRESTClient(c.String("api-url"), cert.AccountID, cert.ServiceKey, logger)
return client
}
func getOriginCertFromContext(c *cli.Context, logger logger.Service) (cert *certutil.OriginCert, originCertPath string, err error) {
originCertPath, err = findOriginCert(c, logger)
if err != nil {
return nil, "", errors.Wrap(err, "Error locating origin cert")
}
blocks, err := readOriginCert(originCertPath, logger)
if err != nil {
return nil, "", errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
}
cert, err = certutil.DecodeOriginCert(blocks)
if err != nil {
return nil, "", errors.Wrap(err, "Error decoding origin cert")
}
if cert.AccountID == "" {
return nil, "", errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
}
return cert, originCertPath, nil
}
func buildRunCommand() *cli.Command {
flags := []cli.Flag{
forceFlag,
credentialsFileFlag,
selectProtocolFlag,
}
flags = append(flags, configureProxyFlags(false)...)
return &cli.Command{
Name: "run",
Action: cliutil.ErrorHandler(runTunnel),
Action: cliutil.ErrorHandler(runCommand),
Before: SetFlagsFromConfigFile,
Usage: "Proxy a local web server by running the given tunnel",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
Flags: []cli.Flag{forceFlag, credentialsFileFlag},
UsageText: "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]",
Description: `Runs the tunnel identified by name or UUUD, creating highly available connections
between your server and the Cloudflare edge. You can provide name or UUID of tunnel to run either as the
last command line argument or in the configuration file using "tunnel: TUNNEL".
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run,
however it does not need access to cert.pem from "cloudflared login" if you identify the tunnel by UUID.
If you experience other problems running the tunnel, "cloudflared tunnel cleanup" may help by removing
any old connection records.
`,
Flags: flags,
CustomHelpTemplate: commandHelpTemplate(),
}
}
func runTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID of the tunnel to run.`)
func runCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
id := c.Args().First()
tunnelID, err := uuid.Parse(id)
if c.NArg() > 1 {
return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`)
}
tunnelRef := c.Args().First()
if tunnelRef == "" {
// see if tunnel id was in the config file
tunnelRef = config.GetConfiguration().TunnelID
if tunnelRef == "" {
return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`)
}
}
return runNamedTunnel(sc, tunnelRef)
}
func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
tunnelID, err := sc.findID(tunnelRef)
if err != nil {
return errors.Wrap(err, "error parsing tunnel ID")
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
sc.logger.Infof("Starting tunnel %s", tunnelID.String())
credentials, err := readTunnelCredentials(c, id, logger)
if err != nil {
return err
}
logger.Debugf("Read credentials for %v", credentials.AccountTag)
return StartServer(c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID})
return sc.run(tunnelID)
}
func buildCleanupCommand() *cli.Command {
return &cli.Command{
Name: "cleanup",
Action: cliutil.ErrorHandler(cleanupConnections),
Usage: "Cleanup connections for the tunnel with given IDs",
ArgsUsage: "TUNNEL-IDS",
Hidden: hideSubcommands,
Name: "cleanup",
Action: cliutil.ErrorHandler(cleanupCommand),
Usage: "Cleanup tunnel connections",
UsageText: "cloudflared tunnel [tunnel command options] cleanup [subcommand options] TUNNEL",
Description: "Delete connections for tunnels with the given UUIDs or names.",
CustomHelpTemplate: commandHelpTemplate(),
}
}
func cleanupConnections(c *cli.Context) error {
func cleanupCommand(c *cli.Context) error {
if c.NArg() < 1 {
return cliutil.UsageError(`"cloudflared tunnel cleanup" requires at least 1 argument, the IDs of the tunnels to cleanup connections.`)
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
cert, _, err := getOriginCertFromContext(c, logger)
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
for i := 0; i < c.NArg(); i++ {
id := c.Args().Get(i)
logger.Infof("Cleanup connection for tunnel %s", id)
if err := client.CleanupConnections(id); err != nil {
logger.Errorf("Error cleaning up connections for tunnel %s, error :%v", id, err)
}
tunnelIDs, err := sc.findIDs(c.Args().Slice())
if err != nil {
return err
}
return sc.cleanupConnections(tunnelIDs)
}
func buildRouteCommand() *cli.Command {
return &cli.Command{
Name: "route",
Action: cliutil.ErrorHandler(routeCommand),
Usage: "Define what hostname or load balancer can route to this tunnel",
UsageText: "cloudflared tunnel [tunnel command options] route [subcommand options] dns|lb TUNNEL HOSTNAME [LB-POOL]",
Description: `The route defines what hostname or load balancer will proxy requests to this tunnel.
To route a hostname by creating a CNAME to tunnel's address:
cloudflared tunnel route dns <tunnel ID> <hostname>
To use this tunnel as a load balancer origin, creating pool and load balancer if necessary:
cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>`,
CustomHelpTemplate: commandHelpTemplate(),
}
}
func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
const (
userHostnameIndex = 2
expectedNArgs = 3
)
if c.NArg() != expectedNArgs {
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
}
userHostname := c.Args().Get(userHostnameIndex)
if userHostname == "" {
return nil, cliutil.UsageError("The third argument should be the hostname")
} else if !validateHostname(userHostname) {
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
}
return tunnelstore.NewDNSRoute(userHostname), nil
}
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
const (
lbNameIndex = 2
lbPoolIndex = 3
expectedNArgs = 4
)
if c.NArg() != expectedNArgs {
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
}
lbName := c.Args().Get(lbNameIndex)
if lbName == "" {
return nil, cliutil.UsageError("The third argument should be the load balancer name")
} else if !validateHostname(lbName) {
return nil, errors.Errorf("%s is not a valid load balancer name", lbName)
}
lbPool := c.Args().Get(lbPoolIndex)
if lbPool == "" {
return nil, cliutil.UsageError("The fourth argument should be the pool name")
} else if !validateName(lbPool) {
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
}
return tunnelstore.NewLBRoute(lbName, lbPool), nil
}
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
func validateName(s string) bool {
return nameRegex.MatchString(s)
}
func validateHostname(s string) bool {
// Slightly stricter than PunyCodeProfile
idnaProfile := idna.New(
idna.ValidateLabels(true),
idna.VerifyDNSLength(true))
puny, err := idnaProfile.ToASCII(s)
return err == nil && validateName(puny)
}
func routeCommand(c *cli.Context) error {
if c.NArg() < 2 {
return cliutil.UsageError(`"cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID or name of the tunnel`)
}
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
const tunnelIDIndex = 1
routeType := c.Args().First()
var route tunnelstore.Route
var tunnelID uuid.UUID
switch routeType {
case "dns":
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
if err != nil {
return err
}
route, err = dnsRouteFromArg(c)
if err != nil {
return err
}
case "lb":
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
if err != nil {
return err
}
route, err = lbRouteFromArg(c)
if err != nil {
return err
}
default:
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
}
res, err := sc.route(tunnelID, route)
if err != nil {
return err
}
sc.logger.Infof(res.SuccessSummary())
return nil
}
func commandHelpTemplate() string {
var parentFlagsHelp string
for _, f := range configureCloudflaredFlags(false) {
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
}
for _, f := range configureLoggingFlags(false) {
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
}
const template = `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.UsageText}}
DESCRIPTION:
{{.Description}}
TUNNEL COMMAND OPTIONS:
%s
SUBCOMMAND OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}
`
return fmt.Sprintf(template, parentFlagsHelp)
}
+42 -3
View File
@@ -40,6 +40,19 @@ func Test_fmtConnections(t *testing.T) {
},
want: "1xDFW",
},
{
name: "with a pending reconnect",
args: args{
connections: []tunnelstore.Connection{
{
ColoName: "DFW",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
IsPendingReconnect: true,
},
},
},
want: "",
},
{
name: "many colos",
args: args{
@@ -67,7 +80,7 @@ func Test_fmtConnections(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fmtConnections(tt.args.connections); got != tt.want {
if got := fmtConnections(tt.args.connections, false); got != tt.want {
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
}
})
@@ -75,11 +88,37 @@ func Test_fmtConnections(t *testing.T) {
}
func TestTunnelfilePath(t *testing.T) {
tunnelID, err := uuid.Parse("f48d8918-bc23-4647-9d48-082c5b76de65")
assert.NoError(t, err)
originCertDir := filepath.Dir("~/.cloudflared/cert.pem")
actual, err := tunnelFilePath("tunnel", originCertDir)
actual, err := tunnelFilePath(tunnelID, originCertDir)
assert.NoError(t, err)
homeDir, err := homedir.Dir()
assert.NoError(t, err)
expected := fmt.Sprintf("%s/.cloudflared/tunnel.json", homeDir)
expected := fmt.Sprintf("%s/.cloudflared/%v.json", homeDir, tunnelID)
assert.Equal(t, expected, actual)
}
func TestValidateName(t *testing.T) {
tests := []struct {
name string
want bool
}{
{name: "", want: false},
{name: "-", want: false},
{name: ".", want: false},
{name: "a b", want: false},
{name: "a+b", want: false},
{name: "-ab", want: false},
{name: "ab", want: true},
{name: "ab-c", want: true},
{name: "abc.def", want: true},
{name: "_ab_c.-d-ef", want: true},
}
for _, tt := range tests {
if got := validateName(tt.name); got != tt.want {
t.Errorf("validateName() = %v, want %v", got, tt.want)
}
}
}
+244
View File
@@ -0,0 +1,244 @@
package ui
import (
"context"
"fmt"
"strings"
"time"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger"
"github.com/gdamore/tcell"
"github.com/rivo/tview"
)
type connState struct {
location string
state status
}
type status int
const (
Disconnected status = iota
Connected
Reconnecting
SetUrl
RegisteringTunnel
)
type TunnelEvent struct {
Index uint8
EventType status
Location string
Url string
}
type uiModel struct {
version string
edgeURL string
metricsURL string
localServices []string
connections []connState
}
type palette struct {
url string
connected string
defaultText string
disconnected string
reconnecting string
}
func NewUIModel(version, hostname, metricsURL string, ing *ingress.Ingress, haConnections int) *uiModel {
localServices := make([]string, len(ing.Rules))
for i, rule := range ing.Rules {
localServices[i] = rule.Service.String()
}
return &uiModel{
version: version,
edgeURL: hostname,
metricsURL: metricsURL,
localServices: localServices,
connections: make([]connState, haConnections),
}
}
func (data *uiModel) LaunchUI(
ctx context.Context,
generalLogger, transportLogger logger.Service,
logLevels []logger.Level,
tunnelEventChan <-chan TunnelEvent,
) {
// Configure the logger to stream logs into the textview
// Add TextView as a group to write output to
logTextView := NewDynamicColorTextView()
generalLogger.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
transportLogger.Add(logTextView, logger.NewUIFormatter(time.RFC3339), logLevels...)
// Construct the UI
palette := palette{
url: "lightblue",
connected: "lime",
defaultText: "white",
disconnected: "red",
reconnecting: "orange",
}
app := tview.NewApplication()
grid := tview.NewGrid().SetGap(1, 0)
frame := tview.NewFrame(grid)
header := fmt.Sprintf("cloudflared [::b]%s", data.version)
frame.AddText(header, true, tview.AlignLeft, tcell.ColorWhite)
// Create table to store connection info and status
connTable := tview.NewTable()
// SetColumns takes a value for each column, representing the size of the column
// Numbers <= 0 represent proportional widths and positive numbers represent absolute widths
grid.SetColumns(20, 0)
// SetRows takes a value for each row, representing the size of the row
grid.SetRows(1, 1, len(data.connections), 1, 0)
// AddItem takes a primitive tview type, row, column, rowSpan, columnSpan, minGridHeight, minGridWidth, and focus
grid.AddItem(tview.NewTextView().SetText("Tunnel:"), 0, 0, 1, 1, 0, 0, false)
grid.AddItem(tview.NewTextView().SetText("Status:"), 1, 0, 1, 1, 0, 0, false)
grid.AddItem(tview.NewTextView().SetText("Connections:"), 2, 0, 1, 1, 0, 0, false)
grid.AddItem(tview.NewTextView().SetText("Metrics:"), 3, 0, 1, 1, 0, 0, false)
tunnelHostText := tview.NewTextView().SetText(data.edgeURL)
grid.AddItem(tunnelHostText, 0, 1, 1, 1, 0, 0, false)
status := fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, strings.Join(data.localServices, ", "))
grid.AddItem(NewDynamicColorTextView().SetText(status), 1, 1, 1, 1, 0, 0, false)
grid.AddItem(connTable, 2, 1, 1, 1, 0, 0, false)
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("Metrics at [%s::b]http://%s/metrics", palette.url, data.metricsURL)), 3, 1, 1, 1, 0, 0, false)
// Add TextView to stream logs
// Logs are displayed in a new grid so a border can be set around them
logGrid := tview.NewGrid().SetBorders(true).AddItem(logTextView.SetChangedFunc(handleNewText(app, logTextView)), 0, 0, 5, 2, 0, 0, false)
// LogFrame holds the Logs header as well as the grid with the textView for streamed logs
logFrame := tview.NewFrame(logGrid).AddText("[::b]Logs:[::-]", true, tview.AlignLeft, tcell.ColorWhite).SetBorders(0, 0, 0, 0, 0, 0)
// Footer for log frame
logFrame.AddText("[::d]Use Ctrl+C to exit[::-]", false, tview.AlignRight, tcell.ColorWhite)
grid.AddItem(logFrame, 4, 0, 5, 2, 0, 0, false)
go func() {
for {
select {
case <-ctx.Done():
app.Stop()
return
case event := <-tunnelEventChan:
switch event.EventType {
case Connected:
data.setConnTableCell(event, connTable, palette)
case Disconnected, Reconnecting:
data.changeConnStatus(event, connTable, generalLogger, palette)
case SetUrl:
tunnelHostText.SetText(event.Url)
data.edgeURL = event.Url
case RegisteringTunnel:
if data.edgeURL == "" {
tunnelHostText.SetText("Registering tunnel...")
}
}
}
app.Draw()
}
}()
go func() {
if err := app.SetRoot(frame, true).Run(); err != nil {
generalLogger.Errorf("Error launching UI: %s", err)
}
}()
}
func NewDynamicColorTextView() *tview.TextView {
return tview.NewTextView().SetDynamicColors(true)
}
// Re-draws application when new logs are streamed to UI
func handleNewText(app *tview.Application, logTextView *tview.TextView) func() {
return func() {
app.Draw()
// SetFocus to enable scrolling in textview
app.SetFocus(logTextView)
}
}
func (data *uiModel) changeConnStatus(event TunnelEvent, table *tview.Table, logger logger.Service, palette palette) {
index := int(event.Index)
// Get connection location and state
connState := data.getConnState(index)
// Check if connection is already displayed in UI
if connState == nil {
logger.Info("Connection is not in the UI table")
return
}
locationState := event.Location
if event.EventType == Disconnected {
connState.state = Disconnected
} else if event.EventType == Reconnecting {
connState.state = Reconnecting
locationState = "Reconnecting..."
}
connectionNum := index + 1
// Get table cell
cell := table.GetCell(index, 0)
// Change dot color in front of text as well as location state
text := newCellText(palette, connectionNum, locationState, event.EventType)
cell.SetText(text)
}
// Return connection location and row in UI table
func (data *uiModel) getConnState(connID int) *connState {
if connID < len(data.connections) {
return &data.connections[connID]
}
return nil
}
func (data *uiModel) setConnTableCell(event TunnelEvent, table *tview.Table, palette palette) {
index := int(event.Index)
connectionNum := index + 1
// Update slice to keep track of connection location and state in UI table
data.connections[index].state = Connected
data.connections[index].location = event.Location
// Update text in table cell to show disconnected state
text := newCellText(palette, connectionNum, event.Location, event.EventType)
cell := tview.NewTableCell(text)
table.SetCell(index, 0, cell)
}
func newCellText(palette palette, connectionNum int, location string, connectedStatus status) string {
// HA connection indicator formatted as: "• #<CONNECTION_INDEX>: <COLO>",
// where the left middle dot's color depends on the status of the connection
const connFmtString = "[%s]\u2022[%s] #%d: %s"
var dotColor string
switch connectedStatus {
case Connected:
dotColor = palette.connected
case Disconnected:
dotColor = palette.disconnected
case Reconnecting:
dotColor = palette.reconnecting
}
return fmt.Sprintf(connFmtString, dotColor, palette.defaultText, connectionNum, location)
}
+26
View File
@@ -0,0 +1,26 @@
package updater
// Version is the functions needed to perform an update
type Version interface {
Apply() error
String() string
}
// Service is the functions to get check for new updates
type Service interface {
Check() (Version, error)
}
const (
// OSKeyName is the url parameter key to send to the checkin API for the operating system of the local cloudflared (e.g. windows, darwin, linux)
OSKeyName = "os"
// ArchitectureKeyName is the url parameter key to send to the checkin API for the architecture of the local cloudflared (e.g. amd64, x86)
ArchitectureKeyName = "arch"
// BetaKeyName is the url parameter key to send to the checkin API to signal if the update should be a beta version or not
BetaKeyName = "beta"
// VersionKeyName is the url parameter key to send to the checkin API to specific what version to upgrade or downgrade to
VersionKeyName = "version"
)
+62 -31
View File
@@ -8,12 +8,11 @@ import (
"runtime"
"time"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/equinox-io/equinox"
"github.com/facebookgo/grace/gracenet"
"github.com/pkg/errors"
)
@@ -25,20 +24,16 @@ const (
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
isManagedInstallFile = ".installedFromPackageManager"
UpdateURL = "https://update.argotunnel.com"
StagingUpdateURL = "https://staging-update.argotunnel.com"
)
var (
publicKey = []byte(`
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
-----END ECDSA PUBLIC KEY-----
`)
version string
)
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
type statusSuccess struct {
newVersion string
}
@@ -64,6 +59,13 @@ func (e *statusErr) ExitCode() int {
return 10
}
type updateOptions struct {
isBeta bool
isStaging bool
isForced bool
version string
}
type UpdateOutcome struct {
Updated bool
Version string
@@ -74,29 +76,44 @@ func (uo *UpdateOutcome) noUpdate() bool {
return uo.Error == nil && uo.Updated == false
}
func checkForUpdateAndApply() UpdateOutcome {
var opts equinox.Options
if err := opts.SetPublicKeyPEM(publicKey); err != nil {
return UpdateOutcome{Error: err}
}
func Init(v string) {
version = v
}
resp, err := equinox.Check(appID, opts)
switch {
case err == equinox.NotAvailableErr:
return UpdateOutcome{}
case err != nil:
return UpdateOutcome{Error: err}
}
err = resp.Apply()
func checkForUpdateAndApply(options updateOptions) UpdateOutcome {
cfdPath, err := os.Executable()
if err != nil {
return UpdateOutcome{Error: err}
}
return UpdateOutcome{Updated: true, Version: resp.ReleaseVersion}
url := UpdateURL
if options.isStaging {
url = StagingUpdateURL
}
s := NewWorkersService(version, url, cfdPath, Options{IsBeta: options.isBeta,
IsForced: options.isForced, RequestedVersion: options.version})
v, err := s.Check()
if err != nil {
return UpdateOutcome{Error: err}
}
//already on the latest version
if v == nil {
return UpdateOutcome{}
}
err = v.Apply()
if err != nil {
return UpdateOutcome{Error: err}
}
return UpdateOutcome{Updated: true, Version: v.String()}
}
func Update(_ *cli.Context) error {
// Update is the handler for the update command from the command line
func Update(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
@@ -107,7 +124,22 @@ func Update(_ *cli.Context) error {
return nil
}
updateOutcome := loggedUpdate(logger)
isBeta := c.Bool("beta")
if isBeta {
logger.Info("cloudflared is set to update to the latest beta version")
}
isStaging := c.Bool("staging")
if isStaging {
logger.Info("cloudflared is set to update from staging")
}
isForced := c.Bool("force")
if isForced {
logger.Info("cloudflared is set to upgrade to the latest publish version regardless of the current version")
}
updateOutcome := loggedUpdate(logger, updateOptions{isBeta: isBeta, isStaging: isStaging, isForced: isForced, version: c.String("version")})
if updateOutcome.Error != nil {
return &statusErr{updateOutcome.Error}
}
@@ -121,8 +153,8 @@ func Update(_ *cli.Context) error {
}
// Checks for an update and applies it if one is available
func loggedUpdate(logger logger.Service) UpdateOutcome {
updateOutcome := checkForUpdateAndApply()
func loggedUpdate(logger logger.Service, options updateOptions) UpdateOutcome {
updateOutcome := checkForUpdateAndApply(options)
if updateOutcome.Updated {
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
}
@@ -168,9 +200,8 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
if a.configurable.enabled {
updateOutcome := loggedUpdate(a.logger)
updateOutcome := loggedUpdate(a.logger, updateOptions{})
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.logger.Info("Restarting service managed by SysV...")
+153
View File
@@ -0,0 +1,153 @@
package updater
import (
"encoding/json"
"errors"
"net/http"
"runtime"
"strconv"
"strings"
)
// Options are the update options supported by the
type Options struct {
// IsBeta is for beta updates to be installed if available
IsBeta bool
// IsForced is to forcibly download the latest version regardless of the current version
IsForced bool
// RequestedVersion is the specific version to upgrade or downgrade to
RequestedVersion string
}
// VersionResponse is the JSON response from the Workers API endpoint
type VersionResponse struct {
URL string `json:"url"`
Version string `json:"version"`
Checksum string `json:"checksum"`
IsCompressed bool `json:"compressed"`
Error string `json:"error"`
}
// WorkersService implements Service.
// It contains everything needed to check in with the WorkersAPI and download and apply the updates
type WorkersService struct {
currentVersion string
url string
targetPath string
opts Options
}
// NewWorkersService creates a new updater Service object.
func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service {
return &WorkersService{
currentVersion: currentVersion,
url: url,
targetPath: targetPath,
opts: opts,
}
}
// Check does a check in with the Workers API to get a new version update
func (s *WorkersService) Check() (Version, error) {
client := &http.Client{
Timeout: clientTimeout,
}
req, err := http.NewRequest(http.MethodGet, s.url, nil)
q := req.URL.Query()
q.Add(OSKeyName, runtime.GOOS)
q.Add(ArchitectureKeyName, runtime.GOARCH)
if s.opts.IsBeta {
q.Add(BetaKeyName, "true")
}
if s.opts.RequestedVersion != "" {
q.Add(VersionKeyName, s.opts.RequestedVersion)
}
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var v VersionResponse
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return nil, err
}
if v.Error != "" {
return nil, errors.New(v.Error)
}
if !s.opts.IsForced && !IsNewerVersion(s.currentVersion, v.Version) {
return nil, nil
}
return NewWorkersVersion(v.URL, v.Version, v.Checksum, s.targetPath, v.IsCompressed), nil
}
// IsNewerVersion checks semantic versioning for the latest version
// cloudflared tagging is more of a date than a semantic version,
// but the same comparision logic still holds for major.minor.patch
// e.g. 2020.8.2 is newer than 2020.8.1.
func IsNewerVersion(current string, check string) bool {
if strings.Contains(strings.ToLower(current), "dev") {
return false // dev builds shouldn't update
}
cMajor, cMinor, cPatch, err := SemanticParts(current)
if err != nil {
return false
}
nMajor, nMinor, nPatch, err := SemanticParts(check)
if err != nil {
return false
}
if nMajor > cMajor {
return true
}
if nMajor == cMajor && nMinor > cMinor {
return true
}
if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
return true
}
return false
}
// SemanticParts gets the major, minor, and patch version of a semantic version string
// e.g. 3.1.2 would return 3, 1, 2, nil
func SemanticParts(version string) (major int, minor int, patch int, err error) {
major = 0
minor = 0
patch = 0
parts := strings.Split(version, ".")
if len(parts) != 3 {
err = errors.New("invalid version")
return
}
major, err = strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return
}
patch, err = strconv.Atoi(parts[2])
if err != nil {
return
}
return
}
@@ -0,0 +1,273 @@
package updater
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"runtime"
"testing"
"github.com/stretchr/testify/require"
)
func respondWithJSON(w http.ResponseWriter, v interface{}, status int) {
data, _ := json.Marshal(v)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(data)
}
func respondWithData(w http.ResponseWriter, b []byte, status int) {
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(status)
w.Write(b)
}
func updateHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.08.05"
host := fmt.Sprintf("http://%s", r.Host)
url := host + "/download"
query := r.URL.Query()
if query.Get(BetaKeyName) == "true" {
version = "2020.08.06"
url = host + "/beta"
}
requestedVersion := query.Get(VersionKeyName)
if requestedVersion != "" {
version = requestedVersion
url = fmt.Sprintf("%s?version=%s", url, requestedVersion)
}
if query.Get(ArchitectureKeyName) != runtime.GOARCH || query.Get(OSKeyName) != runtime.GOOS {
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
return
}
h := sha256.New()
fmt.Fprint(h, version)
checksum := fmt.Sprintf("%x", h.Sum(nil))
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
respondWithJSON(w, v, http.StatusOK)
}
func gzipUpdateHandler(w http.ResponseWriter, r *http.Request) {
log.Println("got a request!")
version := "2020.09.02"
h := sha256.New()
fmt.Fprint(h, version)
checksum := fmt.Sprintf("%x", h.Sum(nil))
url := fmt.Sprintf("http://%s/gzip-download.tgz", r.Host)
v := VersionResponse{URL: url, Version: version, Checksum: checksum}
respondWithJSON(w, v, http.StatusOK)
}
func compressedDownloadHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.09.02"
buf := new(bytes.Buffer)
gw := gzip.NewWriter(buf)
tw := tar.NewWriter(gw)
header := &tar.Header{
Size: int64(len(version)),
Name: "download",
}
tw.WriteHeader(header)
tw.Write([]byte(version))
tw.Close()
gw.Close()
respondWithData(w, buf.Bytes(), http.StatusOK)
}
func downloadHandler(w http.ResponseWriter, r *http.Request) {
version := "2020.08.05"
requestedVersion := r.URL.Query().Get(VersionKeyName)
if requestedVersion != "" {
version = requestedVersion
}
respondWithData(w, []byte(version), http.StatusOK)
}
func betaHandler(w http.ResponseWriter, r *http.Request) {
respondWithData(w, []byte("2020.08.06"), http.StatusOK)
}
func failureHandler(w http.ResponseWriter, r *http.Request) {
respondWithJSON(w, VersionResponse{Error: "unsupported os and architecture"}, http.StatusBadRequest)
}
func createServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc("/updater", updateHandler)
mux.HandleFunc("/download", downloadHandler)
mux.HandleFunc("/beta", betaHandler)
mux.HandleFunc("/fail", failureHandler)
mux.HandleFunc("/compressed", gzipUpdateHandler)
mux.HandleFunc("/gzip-download.tgz", compressedDownloadHandler)
return httptest.NewServer(mux)
}
func createTestFile(t *testing.T, path string) {
f, err := os.Create("tmpfile")
require.NoError(t, err)
fmt.Fprint(f, "2020.08.04")
f.Close()
}
func TestUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
log.Println("server url: ", ts.URL)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.05")
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.05")
}
func TestBetaUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsBeta: true})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.06")
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.06")
}
func TestFailUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/fail", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.Error(t, err)
require.Nil(t, v)
}
func TestNoUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Nil(t, v)
}
func TestForcedUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.5", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{IsForced: true})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, v.String(), "2020.08.05")
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, string(dat), "2020.08.05")
}
func TestUpdateSpecificVersionService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
reqVersion := "2020.9.1"
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/updater", ts.URL), testFilePath, Options{RequestedVersion: reqVersion})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, reqVersion, v.String())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, reqVersion, string(dat))
}
func TestCompressedUpdateService(t *testing.T) {
ts := createServer()
defer ts.Close()
testFilePath := "tmpfile"
createTestFile(t, testFilePath)
defer os.Remove(testFilePath)
s := NewWorkersService("2020.8.2", fmt.Sprintf("%s/compressed", ts.URL), testFilePath, Options{})
v, err := s.Check()
require.NoError(t, err)
require.Equal(t, "2020.09.02", v.String())
require.NoError(t, v.Apply())
dat, err := ioutil.ReadFile(testFilePath)
require.NoError(t, err)
require.Equal(t, "2020.09.02", string(dat))
}
func TestVersionParsing(t *testing.T) {
require.False(t, IsNewerVersion("2020.8.2", "2020.8.2"))
require.True(t, IsNewerVersion("2020.8.2", "2020.8.3"))
require.True(t, IsNewerVersion("2020.8.2", "2021.1.2"))
require.True(t, IsNewerVersion("2020.8.2", "2020.9.1"))
require.True(t, IsNewerVersion("2020.8.2", "2020.12.45"))
require.False(t, IsNewerVersion("2020.8.2", "2020.6.3"))
require.False(t, IsNewerVersion("DEV", "2020.8.5"))
require.False(t, IsNewerVersion("2020.8.2", "asdlkfjasdf"))
require.True(t, IsNewerVersion("3.0.1", "4.2.1"))
}
+230
View File
@@ -0,0 +1,230 @@
package updater
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"time"
)
const (
clientTimeout = time.Second * 60
// stop the service
// rename cloudflared.exe to cloudflared.exe.old
// rename cloudflared.exe.new to cloudflared.exe
// delete cloudflared.exe.old
// start the service
// delete the batch file
windowsUpdateCommandTemplate = `@echo off
sc stop cloudflared >nul 2>&1
rename "{{.TargetPath}}" {{.OldName}}
rename "{{.NewPath}}" {{.BinaryName}}
del "{{.OldPath}}"
sc start cloudflared >nul 2>&1
del {{.BatchName}}`
batchFileName = "cfd_update.bat"
)
// Prepare some data to insert into the template.
type batchData struct {
TargetPath string
OldName string
NewPath string
OldPath string
BinaryName string
BatchName string
}
// WorkersVersion implements the Version interface.
// It contains everything needed to preform a version upgrade
type WorkersVersion struct {
downloadURL string
checksum string
version string
targetPath string
isCompressed bool
}
// NewWorkersVersion creates a new Version object. This is normally created by a WorkersService JSON checkin response
// url is where to download the file
// version is the version of this update
// checksum is the expected checksum of the downloaded file
// target path is where the file should be replace. Normally this the running cloudflared's path
func NewWorkersVersion(url, version, checksum, targetPath string, isCompressed bool) Version {
return &WorkersVersion{downloadURL: url, version: version, checksum: checksum, targetPath: targetPath, isCompressed: isCompressed}
}
// Apply does the actual verification and update logic.
// This includes signature and checksum validation,
// replacing the binary, etc
func (v *WorkersVersion) Apply() error {
newFilePath := fmt.Sprintf("%s.new", v.targetPath)
os.Remove(newFilePath) //remove any failed updates before download
// download the file
if err := download(v.downloadURL, newFilePath, v.isCompressed); err != nil {
return err
}
// check that the file is what is expected
if err := isValidChecksum(v.checksum, newFilePath); err != nil {
return err
}
oldFilePath := fmt.Sprintf("%s.old", v.targetPath)
// Windows requires more effort to self update, especially when it is running as a service:
// you have to stop the service (if running as one) in order to move/rename the binary
// but now the binary isn't running though, so an external process
// has to move the old binary out and the new one in then start the service
// the easiest way to do this is with a batch file (or with a DLL, but that gets ugly for a cross compiled binary like cloudflared)
// a batch file isn't ideal, but it is the simplest path forward for the constraints Windows creates
if runtime.GOOS == "windows" {
if err := writeBatchFile(v.targetPath, newFilePath, oldFilePath); err != nil {
return err
}
rootDir := filepath.Dir(v.targetPath)
batchPath := filepath.Join(rootDir, batchFileName)
return runWindowsBatch(batchPath)
}
// now move the current file out, move the new file in and delete the old file
if err := os.Rename(v.targetPath, oldFilePath); err != nil {
return err
}
if err := os.Rename(newFilePath, v.targetPath); err != nil {
//attempt rollback
os.Rename(oldFilePath, v.targetPath)
return err
}
os.Remove(oldFilePath)
return nil
}
// String returns the version number of this update/release (e.g. 2020.08.05)
func (v *WorkersVersion) String() string {
return v.version
}
// download the file from the link in the json
func download(url, filepath string, isCompressed bool) error {
client := &http.Client{
Timeout: clientTimeout,
}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
var r io.Reader
r = resp.Body
// compressed macos binary, need to decompress
if isCompressed || isCompressedFile(url) {
// first the gzip reader
gr, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}
defer gr.Close()
// now the tar
tr := tar.NewReader(gr)
// advance the reader pass the header, which will be the single binary file
tr.Next()
r = tr
}
out, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
}
// isCompressedFile is a really simple file extension check to see if this is a macos tar and gzipped
func isCompressedFile(urlstring string) bool {
if strings.HasSuffix(urlstring, ".tgz") {
return true
}
u, err := url.Parse(urlstring)
if err != nil {
return false
}
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 {
os.Remove(batchFileName) //remove any failed updates before download
f, err := os.Create(batchFileName)
if err != nil {
return err
}
defer f.Close()
cfdName := filepath.Base(targetPath)
oldName := filepath.Base(oldPath)
data := batchData{
TargetPath: targetPath,
OldName: oldName,
NewPath: newPath,
OldPath: oldPath,
BinaryName: cfdName,
BatchName: batchFileName,
}
t, err := template.New("batch").Parse(windowsUpdateCommandTemplate)
if err != nil {
return err
}
return t.Execute(f, data)
}
// run each OS command for windows
func runWindowsBatch(batchFile string) error {
cmd := exec.Command("cmd", "/c", batchFile)
return cmd.Start()
}
+4 -5
View File
@@ -14,7 +14,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
@@ -150,12 +150,11 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
switch c.Cmd {
case svc.Interrogate:
statusChan <- c.CurrentStatus
case svc.Stop:
case svc.Stop, svc.Shutdown:
close(s.graceShutdownC)
statusChan <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted}
statusChan <- svc.Status{State: svc.StopPending}
case svc.Shutdown:
close(s.shutdownC)
statusChan <- svc.Status{State: svc.StopPending}
return
default:
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
}
+62
View File
@@ -0,0 +1,62 @@
package connection
import (
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
)
const (
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
)
type Config struct {
OriginClient OriginClient
GracePeriod time.Duration
ReplaceExisting bool
}
type NamedTunnelConfig struct {
Auth pogs.TunnelAuth
ID uuid.UUID
Client pogs.ClientInfo
}
type ClassicTunnelConfig struct {
Hostname string
OriginCert []byte
// feature-flag to use new edge reconnect tokens
UseReconnectToken bool
}
func (c *ClassicTunnelConfig) IsTrialZone() bool {
return c.Hostname == ""
}
type OriginClient interface {
Proxy(w ResponseWriter, req *http.Request, isWebsocket bool) error
}
type ResponseWriter interface {
WriteRespHeaders(*http.Response) error
WriteErrorResponse()
io.ReadWriter
}
type ConnectedFuse interface {
Connected()
IsConnected() bool
}
func uint8ToString(input uint8) string {
return strconv.FormatUint(uint64(input), 10)
}
func isServerSentEvent(headers http.Header) bool {
return strings.ToLower(headers.Get("content-type")) == "text/event-stream"
}
+113
View File
@@ -0,0 +1,113 @@
package connection
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/logger"
"github.com/gobwas/ws/wsutil"
)
const (
largeFileSize = 2 * 1024 * 1024
)
var (
testConfig = &Config{
OriginClient: &mockOriginClient{},
GracePeriod: time.Millisecond * 100,
}
testLogger, _ = logger.New()
testOriginURL = &url.URL{
Scheme: "https",
Host: "connectiontest.argotunnel.com",
}
testTunnelEventChan = make(chan ui.TunnelEvent)
testObserver = &Observer{
testLogger,
m,
testTunnelEventChan,
}
testLargeResp = make([]byte, largeFileSize)
)
type testRequest struct {
name string
endpoint string
expectedStatus int
expectedBody []byte
isProxyError bool
}
type mockOriginClient struct {
}
func (moc *mockOriginClient) Proxy(w ResponseWriter, r *http.Request, isWebsocket bool) error {
if isWebsocket {
return wsEndpoint(w, r)
}
switch r.URL.Path {
case "/ok":
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
case "/large_file":
originRespEndpoint(w, http.StatusOK, testLargeResp)
case "/400":
originRespEndpoint(w, http.StatusBadRequest, []byte(http.StatusText(http.StatusBadRequest)))
case "/500":
originRespEndpoint(w, http.StatusInternalServerError, []byte(http.StatusText(http.StatusInternalServerError)))
case "/error":
return fmt.Errorf("Failed to proxy to origin")
default:
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
}
return nil
}
type nowriter struct {
io.Reader
}
func (nowriter) Write(p []byte) (int, error) {
return 0, fmt.Errorf("Writer not implemented")
}
func wsEndpoint(w ResponseWriter, r *http.Request) error {
resp := &http.Response{
StatusCode: http.StatusSwitchingProtocols,
}
w.WriteRespHeaders(resp)
clientReader := nowriter{r.Body}
go func() {
for {
data, err := wsutil.ReadClientText(clientReader)
if err != nil {
return
}
if err := wsutil.WriteServerText(w, data); err != nil {
return
}
}
}()
<-r.Context().Done()
return nil
}
func originRespEndpoint(w ResponseWriter, status int, data []byte) {
resp := &http.Response{
StatusCode: status,
}
w.WriteRespHeaders(resp)
w.Write(data)
}
type mockConnectedFuse struct{}
func (mcf mockConnectedFuse) Connected() {}
func (mcf mockConnectedFuse) IsConnected() bool {
return true
}
+76
View File
@@ -0,0 +1,76 @@
package connection
import (
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/prometheus/client_golang/prometheus"
)
const (
DuplicateConnectionError = "EDUPCONN"
)
// RegisterTunnel error from client
type clientRegisterTunnelError struct {
cause error
}
func newRPCError(cause error, counter *prometheus.CounterVec, name rpcName) clientRegisterTunnelError {
counter.WithLabelValues(cause.Error(), string(name)).Inc()
return clientRegisterTunnelError{cause: cause}
}
func (e clientRegisterTunnelError) Error() string {
return e.cause.Error()
}
type DupConnRegisterTunnelError struct{}
var errDuplicationConnection = &DupConnRegisterTunnelError{}
func (e DupConnRegisterTunnelError) Error() string {
return "already connected to this server, trying another address"
}
// RegisterTunnel error from server
type serverRegisterTunnelError struct {
cause error
permanent bool
}
func (e serverRegisterTunnelError) Error() string {
return e.cause.Error()
}
func serverRegistrationErrorFromRPC(err error) *serverRegisterTunnelError {
if retryable, ok := err.(*tunnelpogs.RetryableError); ok {
return &serverRegisterTunnelError{
cause: retryable.Unwrap(),
permanent: false,
}
}
return &serverRegisterTunnelError{
cause: err,
permanent: true,
}
}
type muxerShutdownError struct{}
func (e muxerShutdownError) Error() string {
return "muxer shutdown"
}
func isHandshakeErrRecoverable(err error, connIndex uint8, observer *Observer) bool {
switch err.(type) {
case edgediscovery.DialError:
observer.Errorf("Connection %d unable to dial edge: %s", connIndex, err)
case h2mux.MuxerHandshakeError:
observer.Errorf("Connection %d handshake with edge server failed: %s", connIndex, err)
default:
observer.Errorf("Connection %d failed: %s", connIndex, err)
return false
}
return true
}
+220
View File
@@ -0,0 +1,220 @@
package connection
import (
"context"
"net"
"net/http"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/websocket"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
const (
muxerTimeout = 5 * time.Second
openStreamTimeout = 30 * time.Second
)
type h2muxConnection struct {
config *Config
muxerConfig *MuxerConfig
muxer *h2mux.Muxer
// connectionID is only used by metrics, and prometheus requires labels to be string
connIndexStr string
connIndex uint8
observer *Observer
}
type MuxerConfig struct {
HeartbeatInterval time.Duration
MaxHeartbeats uint64
CompressionSetting h2mux.CompressionSetting
MetricsUpdateFreq time.Duration
}
func (mc *MuxerConfig) H2MuxerConfig(h h2mux.MuxedStreamHandler, logger logger.Service) *h2mux.MuxerConfig {
return &h2mux.MuxerConfig{
Timeout: muxerTimeout,
Handler: h,
IsClient: true,
HeartbeatInterval: mc.HeartbeatInterval,
MaxHeartbeats: mc.MaxHeartbeats,
Logger: logger,
CompressionQuality: mc.CompressionSetting,
}
}
// NewTunnelHandler returns a TunnelHandler, origin LAN IP and error
func NewH2muxConnection(ctx context.Context,
config *Config,
muxerConfig *MuxerConfig,
edgeConn net.Conn,
connIndex uint8,
observer *Observer,
) (*h2muxConnection, error, bool) {
h := &h2muxConnection{
config: config,
muxerConfig: muxerConfig,
connIndexStr: uint8ToString(connIndex),
connIndex: connIndex,
observer: observer,
}
// Establish a muxed connection with the edge
// Client mux handshake with agent server
muxer, err := h2mux.Handshake(edgeConn, edgeConn, *muxerConfig.H2MuxerConfig(h, observer), h2mux.ActiveStreams)
if err != nil {
recoverable := isHandshakeErrRecoverable(err, connIndex, observer)
return nil, err, recoverable
}
h.muxer = muxer
return h, nil, false
}
func (h *h2muxConnection) ServeNamedTunnel(ctx context.Context, namedTunnel *NamedTunnelConfig, credentialManager CredentialManager, connOptions *tunnelpogs.ConnectionOptions, connectedFuse ConnectedFuse) error {
errGroup, serveCtx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
return h.serveMuxer(serveCtx)
})
errGroup.Go(func() error {
stream, err := h.newRPCStream(serveCtx, register)
if err != nil {
return err
}
rpcClient := newRegistrationRPCClient(ctx, stream, h.observer)
defer rpcClient.Close()
if err = rpcClient.RegisterConnection(serveCtx, namedTunnel, connOptions, h.connIndex, h.observer); err != nil {
return err
}
connectedFuse.Connected()
return nil
})
errGroup.Go(func() error {
h.controlLoop(serveCtx, connectedFuse, true)
return nil
})
return errGroup.Wait()
}
func (h *h2muxConnection) ServeClassicTunnel(ctx context.Context, classicTunnel *ClassicTunnelConfig, credentialManager CredentialManager, registrationOptions *tunnelpogs.RegistrationOptions, connectedFuse ConnectedFuse) error {
errGroup, serveCtx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
return h.serveMuxer(serveCtx)
})
errGroup.Go(func() (err error) {
defer func() {
if err == nil {
connectedFuse.Connected()
}
}()
if classicTunnel.UseReconnectToken && connectedFuse.IsConnected() {
err := h.reconnectTunnel(ctx, credentialManager, classicTunnel, registrationOptions)
if err == nil {
return nil
}
// log errors and proceed to RegisterTunnel
h.observer.Errorf("Couldn't reconnect connection %d. Reregistering it instead. Error was: %v", h.connIndex, err)
}
return h.registerTunnel(ctx, credentialManager, classicTunnel, registrationOptions)
})
errGroup.Go(func() error {
h.controlLoop(serveCtx, connectedFuse, false)
return nil
})
return errGroup.Wait()
}
func (h *h2muxConnection) serveMuxer(ctx context.Context) error {
// All routines should stop when muxer finish serving. When muxer is shutdown
// gracefully, it doesn't return an error, so we need to return errMuxerShutdown
// here to notify other routines to stop
err := h.muxer.Serve(ctx)
if err == nil {
return muxerShutdownError{}
}
return err
}
func (h *h2muxConnection) controlLoop(ctx context.Context, connectedFuse ConnectedFuse, isNamedTunnel bool) {
updateMetricsTickC := time.Tick(h.muxerConfig.MetricsUpdateFreq)
for {
select {
case <-ctx.Done():
// UnregisterTunnel blocks until the RPC call returns
if connectedFuse.IsConnected() {
h.unregister(isNamedTunnel)
}
h.muxer.Shutdown()
return
case <-updateMetricsTickC:
h.observer.metrics.updateMuxerMetrics(h.connIndexStr, h.muxer.Metrics())
}
}
}
func (h *h2muxConnection) newRPCStream(ctx context.Context, rpcName rpcName) (*h2mux.MuxedStream, error) {
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
defer openStreamCancel()
stream, err := h.muxer.OpenRPCStream(openStreamCtx)
if err != nil {
return nil, err
}
return stream, nil
}
func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
respWriter := &h2muxRespWriter{stream}
req, reqErr := h.newRequest(stream)
if reqErr != nil {
respWriter.WriteErrorResponse()
return reqErr
}
err := h.config.OriginClient.Proxy(respWriter, req, websocket.IsWebSocketUpgrade(req))
if err != nil {
respWriter.WriteErrorResponse()
return err
}
return nil
}
func (h *h2muxConnection) newRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
req, err := http.NewRequest("GET", "http://localhost:8080", h2mux.MuxedStreamReader{MuxedStream: stream})
if err != nil {
return nil, errors.Wrap(err, "Unexpected error from http.NewRequest")
}
err = h2mux.H2RequestHeadersToH1Request(stream.Headers, req)
if err != nil {
return nil, errors.Wrap(err, "invalid request received")
}
return req, nil
}
type h2muxRespWriter struct {
*h2mux.MuxedStream
}
func (rp *h2muxRespWriter) WriteRespHeaders(resp *http.Response) error {
headers := h2mux.H1ResponseToH2ResponseHeaders(resp)
headers = append(headers, h2mux.Header{Name: responseMetaHeaderField, Value: responseMetaHeaderOrigin})
return rp.WriteHeaders(headers)
}
func (rp *h2muxRespWriter) WriteErrorResponse() {
rp.WriteHeaders([]h2mux.Header{
{Name: ":status", Value: "502"},
{Name: responseMetaHeaderField, Value: responseMetaHeaderCfd},
})
rp.Write([]byte("502 Bad Gateway"))
}
+242
View File
@@ -0,0 +1,242 @@
package connection
import (
"context"
"fmt"
"io"
"net"
"net/http"
"strconv"
"sync"
"testing"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/gobwas/ws/wsutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
testMuxerConfig = &MuxerConfig{
HeartbeatInterval: time.Second * 5,
MaxHeartbeats: 5,
CompressionSetting: 0,
MetricsUpdateFreq: time.Second * 5,
}
)
func newH2MuxConnection(ctx context.Context, t require.TestingT) (*h2muxConnection, *h2mux.Muxer) {
edgeConn, originConn := net.Pipe()
edgeMuxChan := make(chan *h2mux.Muxer)
go func() {
edgeMuxConfig := h2mux.MuxerConfig{
Logger: testObserver,
}
edgeMux, err := h2mux.Handshake(edgeConn, edgeConn, edgeMuxConfig, h2mux.ActiveStreams)
require.NoError(t, err)
edgeMuxChan <- edgeMux
}()
var connIndex = uint8(0)
h2muxConn, err, _ := NewH2muxConnection(ctx, testConfig, testMuxerConfig, originConn, connIndex, testObserver)
require.NoError(t, err)
return h2muxConn, <-edgeMuxChan
}
func TestServeStreamHTTP(t *testing.T) {
tests := []testRequest{
{
name: "ok",
endpoint: "/ok",
expectedStatus: http.StatusOK,
expectedBody: []byte(http.StatusText(http.StatusOK)),
},
{
name: "large_file",
endpoint: "/large_file",
expectedStatus: http.StatusOK,
expectedBody: testLargeResp,
},
{
name: "Bad request",
endpoint: "/400",
expectedStatus: http.StatusBadRequest,
expectedBody: []byte(http.StatusText(http.StatusBadRequest)),
},
{
name: "Internal server error",
endpoint: "/500",
expectedStatus: http.StatusInternalServerError,
expectedBody: []byte(http.StatusText(http.StatusInternalServerError)),
},
{
name: "Proxy error",
endpoint: "/error",
expectedStatus: http.StatusBadGateway,
expectedBody: nil,
isProxyError: true,
},
}
ctx, cancel := context.WithCancel(context.Background())
h2muxConn, edgeMux := newH2MuxConnection(ctx, t)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
edgeMux.Serve(ctx)
}()
go func() {
defer wg.Done()
err := h2muxConn.serveMuxer(ctx)
require.Error(t, err)
}()
for _, test := range tests {
headers := []h2mux.Header{
{
Name: ":path",
Value: test.endpoint,
},
}
stream, err := edgeMux.OpenStream(ctx, headers, nil)
require.NoError(t, err)
require.True(t, hasHeader(stream, ":status", strconv.Itoa(test.expectedStatus)))
if test.isProxyError {
assert.True(t, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderCfd))
} else {
assert.True(t, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderOrigin))
body := make([]byte, len(test.expectedBody))
_, err = stream.Read(body)
require.NoError(t, err)
require.Equal(t, test.expectedBody, body)
}
}
cancel()
wg.Wait()
}
func TestServeStreamWS(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
h2muxConn, edgeMux := newH2MuxConnection(ctx, t)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
edgeMux.Serve(ctx)
}()
go func() {
defer wg.Done()
err := h2muxConn.serveMuxer(ctx)
require.Error(t, err)
}()
headers := []h2mux.Header{
{
Name: ":path",
Value: "/ws",
},
{
Name: "connection",
Value: "upgrade",
},
{
Name: "upgrade",
Value: "websocket",
},
}
readPipe, writePipe := io.Pipe()
stream, err := edgeMux.OpenStream(ctx, headers, readPipe)
require.NoError(t, err)
require.True(t, hasHeader(stream, ":status", strconv.Itoa(http.StatusSwitchingProtocols)))
assert.True(t, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderOrigin))
data := []byte("test websocket")
err = wsutil.WriteClientText(writePipe, data)
require.NoError(t, err)
respBody, err := wsutil.ReadServerText(stream)
require.NoError(t, err)
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
cancel()
wg.Wait()
}
func hasHeader(stream *h2mux.MuxedStream, name, val string) bool {
for _, header := range stream.Headers {
if header.Name == name && header.Value == val {
return true
}
}
return false
}
func benchmarkServeStreamHTTPSimple(b *testing.B, test testRequest) {
ctx, cancel := context.WithCancel(context.Background())
h2muxConn, edgeMux := newH2MuxConnection(ctx, b)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
edgeMux.Serve(ctx)
}()
go func() {
defer wg.Done()
err := h2muxConn.serveMuxer(ctx)
require.Error(b, err)
}()
headers := []h2mux.Header{
{
Name: ":path",
Value: test.endpoint,
},
}
body := make([]byte, len(test.expectedBody))
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StartTimer()
stream, openstreamErr := edgeMux.OpenStream(ctx, headers, nil)
_, readBodyErr := stream.Read(body)
b.StopTimer()
require.NoError(b, openstreamErr)
assert.True(b, hasHeader(stream, responseMetaHeaderField, responseMetaHeaderOrigin))
require.True(b, hasHeader(stream, ":status", strconv.Itoa(http.StatusOK)))
require.NoError(b, readBodyErr)
require.Equal(b, test.expectedBody, body)
}
cancel()
wg.Wait()
}
func BenchmarkServeStreamHTTPSimple(b *testing.B) {
test := testRequest{
name: "ok",
endpoint: "/ok",
expectedStatus: http.StatusOK,
expectedBody: []byte(http.StatusText(http.StatusOK)),
}
benchmarkServeStreamHTTPSimple(b, test)
}
func BenchmarkServeStreamHTTPLargeFile(b *testing.B) {
test := testRequest{
name: "large_file",
endpoint: "/large_file",
expectedStatus: http.StatusOK,
expectedBody: testLargeResp,
}
benchmarkServeStreamHTTPSimple(b, test)
}
+31
View File
@@ -0,0 +1,31 @@
package connection
import (
"fmt"
"net/http"
"github.com/cloudflare/cloudflared/h2mux"
)
const (
responseMetaHeaderField = "cf-cloudflared-response-meta"
)
var (
canonicalResponseUserHeadersField = http.CanonicalHeaderKey(h2mux.ResponseUserHeadersField)
canonicalResponseMetaHeaderField = http.CanonicalHeaderKey(responseMetaHeaderField)
responseMetaHeaderCfd = mustInitRespMetaHeader("cloudflared")
responseMetaHeaderOrigin = mustInitRespMetaHeader("origin")
)
type responseMetaHeader struct {
Source string `json:"src"`
}
func mustInitRespMetaHeader(src string) string {
header, err := json.Marshal(responseMetaHeader{Source: src})
if err != nil {
panic(fmt.Sprintf("Failed to serialize response meta header = %s, err: %v", src, err))
}
return string(header)
}
+221
View File
@@ -0,0 +1,221 @@
package connection
import (
"context"
"errors"
"io"
"math"
"net"
"net/http"
"strings"
"sync"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"golang.org/x/net/http2"
)
const (
internalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade"
websocketUpgrade = "websocket"
controlStreamUpgrade = "control-stream"
)
var (
errNotFlusher = errors.New("ResponseWriter doesn't implement http.Flusher")
)
type http2Connection struct {
conn net.Conn
server *http2.Server
config *Config
namedTunnel *NamedTunnelConfig
connOptions *tunnelpogs.ConnectionOptions
observer *Observer
connIndexStr string
connIndex uint8
wg *sync.WaitGroup
// newRPCClientFunc allows us to mock RPCs during testing
newRPCClientFunc func(context.Context, io.ReadWriteCloser, logger.Service) NamedTunnelRPCClient
connectedFuse ConnectedFuse
}
func NewHTTP2Connection(
conn net.Conn,
config *Config,
namedTunnelConfig *NamedTunnelConfig,
connOptions *tunnelpogs.ConnectionOptions,
observer *Observer,
connIndex uint8,
connectedFuse ConnectedFuse,
) *http2Connection {
return &http2Connection{
conn: conn,
server: &http2.Server{
MaxConcurrentStreams: math.MaxUint32,
},
config: config,
namedTunnel: namedTunnelConfig,
connOptions: connOptions,
observer: observer,
connIndexStr: uint8ToString(connIndex),
connIndex: connIndex,
wg: &sync.WaitGroup{},
newRPCClientFunc: newRegistrationRPCClient,
connectedFuse: connectedFuse,
}
}
func (c *http2Connection) Serve(ctx context.Context) {
go func() {
<-ctx.Done()
c.close()
}()
c.server.ServeConn(c.conn, &http2.ServeConnOpts{
Context: ctx,
Handler: c,
})
}
func (c *http2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.wg.Add(1)
defer c.wg.Done()
respWriter := &http2RespWriter{
r: r.Body,
w: w,
}
flusher, isFlusher := w.(http.Flusher)
if !isFlusher {
c.observer.Errorf("%T doesn't implement http.Flusher", w)
respWriter.WriteErrorResponse()
return
}
respWriter.flusher = flusher
var err error
if isControlStreamUpgrade(r) {
respWriter.shouldFlush = true
err = c.serveControlStream(r.Context(), respWriter)
} else if isWebsocketUpgrade(r) {
respWriter.shouldFlush = true
stripWebsocketUpgradeHeader(r)
err = c.config.OriginClient.Proxy(respWriter, r, true)
} else {
err = c.config.OriginClient.Proxy(respWriter, r, false)
}
if err != nil {
respWriter.WriteErrorResponse()
}
}
func (c *http2Connection) serveControlStream(ctx context.Context, respWriter *http2RespWriter) error {
rpcClient := c.newRPCClientFunc(ctx, respWriter, c.observer)
defer rpcClient.Close()
if err := rpcClient.RegisterConnection(ctx, c.namedTunnel, c.connOptions, c.connIndex, c.observer); err != nil {
return err
}
c.connectedFuse.Connected()
<-ctx.Done()
rpcClient.GracefulShutdown(ctx, c.config.GracePeriod)
return nil
}
func (c *http2Connection) close() {
// Wait for all serve HTTP handlers to return
c.wg.Wait()
c.conn.Close()
}
type http2RespWriter struct {
r io.Reader
w http.ResponseWriter
flusher http.Flusher
shouldFlush bool
}
func (rp *http2RespWriter) WriteRespHeaders(resp *http.Response) error {
dest := rp.w.Header()
userHeaders := make(http.Header, len(resp.Header))
for header, values := range resp.Header {
// Since these are http2 headers, they're required to be lowercase
h2name := strings.ToLower(header)
for _, v := range values {
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.
dest.Add(h2name, v)
// Since these are http2 headers, they're required to be lowercase
} else if !h2mux.IsControlHeader(h2name) || h2mux.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.Add(h2name, v)
}
}
}
// Perform user header serialization and set them in the single header
dest.Set(canonicalResponseUserHeadersField, h2mux.SerializeHeaders(userHeaders))
rp.setResponseMetaHeader(responseMetaHeaderOrigin)
status := resp.StatusCode
// HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1
if status == http.StatusSwitchingProtocols {
status = http.StatusOK
}
rp.w.WriteHeader(status)
if isServerSentEvent(resp.Header) {
rp.shouldFlush = true
}
if rp.shouldFlush {
rp.flusher.Flush()
}
return nil
}
func (rp *http2RespWriter) WriteErrorResponse() {
rp.setResponseMetaHeader(responseMetaHeaderCfd)
rp.w.WriteHeader(http.StatusBadGateway)
}
func (rp *http2RespWriter) setResponseMetaHeader(value string) {
rp.w.Header().Set(canonicalResponseMetaHeaderField, value)
}
func (rp *http2RespWriter) Read(p []byte) (n int, err error) {
return rp.r.Read(p)
}
func (rp *http2RespWriter) Write(p []byte) (n int, err error) {
defer func() {
// Implementer of OriginClient should make sure it doesn't write to the connection after Proxy returns
// Register a recover routine just in case.
if r := recover(); r != nil {
println("Recover from http2 response writer panic, error", r)
}
}()
n, err = rp.w.Write(p)
if err == nil && rp.shouldFlush {
rp.flusher.Flush()
}
return n, err
}
func (rp *http2RespWriter) Close() error {
return nil
}
func isControlStreamUpgrade(r *http.Request) bool {
return strings.ToLower(r.Header.Get(internalUpgradeHeader)) == controlStreamUpgrade
}
func isWebsocketUpgrade(r *http.Request) bool {
return strings.ToLower(r.Header.Get(internalUpgradeHeader)) == websocketUpgrade
}
func stripWebsocketUpgradeHeader(r *http.Request) {
r.Header.Del(internalUpgradeHeader)
}
+303
View File
@@ -0,0 +1,303 @@
package connection
import (
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/gobwas/ws/wsutil"
"github.com/stretchr/testify/require"
"golang.org/x/net/http2"
)
var (
testTransport = http2.Transport{}
)
func newTestHTTP2Connection() (*http2Connection, net.Conn) {
edgeConn, originConn := net.Pipe()
var connIndex = uint8(0)
return NewHTTP2Connection(
originConn,
testConfig,
&NamedTunnelConfig{},
&pogs.ConnectionOptions{},
testObserver,
connIndex,
mockConnectedFuse{},
), edgeConn
}
func TestServeHTTP(t *testing.T) {
tests := []testRequest{
{
name: "ok",
endpoint: "ok",
expectedStatus: http.StatusOK,
expectedBody: []byte(http.StatusText(http.StatusOK)),
},
{
name: "large_file",
endpoint: "large_file",
expectedStatus: http.StatusOK,
expectedBody: testLargeResp,
},
{
name: "Bad request",
endpoint: "400",
expectedStatus: http.StatusBadRequest,
expectedBody: []byte(http.StatusText(http.StatusBadRequest)),
},
{
name: "Internal server error",
endpoint: "500",
expectedStatus: http.StatusInternalServerError,
expectedBody: []byte(http.StatusText(http.StatusInternalServerError)),
},
{
name: "Proxy error",
endpoint: "error",
expectedStatus: http.StatusBadGateway,
expectedBody: nil,
isProxyError: true,
},
}
http2Conn, edgeConn := newTestHTTP2Connection()
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
http2Conn.Serve(ctx)
}()
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
require.NoError(t, err)
for _, test := range tests {
endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
require.NoError(t, err)
resp, err := edgeHTTP2Conn.RoundTrip(req)
require.NoError(t, err)
require.Equal(t, test.expectedStatus, resp.StatusCode)
if test.expectedBody != nil {
respBody, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, test.expectedBody, respBody)
}
if test.isProxyError {
require.Equal(t, responseMetaHeaderCfd, resp.Header.Get(responseMetaHeaderField))
} else {
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(responseMetaHeaderField))
}
}
cancel()
wg.Wait()
}
type mockNamedTunnelRPCClient struct {
registered chan struct{}
unregistered chan struct{}
}
func (mc mockNamedTunnelRPCClient) RegisterConnection(
c context.Context,
config *NamedTunnelConfig,
options *tunnelpogs.ConnectionOptions,
connIndex uint8,
observer *Observer,
) error {
close(mc.registered)
return nil
}
func (mc mockNamedTunnelRPCClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) {
close(mc.unregistered)
}
func (mockNamedTunnelRPCClient) Close() {}
type mockRPCClientFactory struct {
registered chan struct{}
unregistered chan struct{}
}
func (mf *mockRPCClientFactory) newMockRPCClient(context.Context, io.ReadWriteCloser, logger.Service) NamedTunnelRPCClient {
return mockNamedTunnelRPCClient{
registered: mf.registered,
unregistered: mf.unregistered,
}
}
type wsRespWriter struct {
*httptest.ResponseRecorder
readPipe *io.PipeReader
writePipe *io.PipeWriter
}
func newWSRespWriter() *wsRespWriter {
readPipe, writePipe := io.Pipe()
return &wsRespWriter{
httptest.NewRecorder(),
readPipe,
writePipe,
}
}
func (w *wsRespWriter) RespBody() io.ReadWriter {
return nowriter{w.readPipe}
}
func (w *wsRespWriter) Write(data []byte) (n int, err error) {
return w.writePipe.Write(data)
}
func TestServeWS(t *testing.T) {
http2Conn, _ := newTestHTTP2Connection()
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
http2Conn.Serve(ctx)
}()
respWriter := newWSRespWriter()
readPipe, writePipe := io.Pipe()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/ws", readPipe)
require.NoError(t, err)
req.Header.Set(internalUpgradeHeader, websocketUpgrade)
wg.Add(1)
go func() {
defer wg.Done()
http2Conn.ServeHTTP(respWriter, req)
}()
data := []byte("test websocket")
err = wsutil.WriteClientText(writePipe, data)
require.NoError(t, err)
respBody, err := wsutil.ReadServerText(respWriter.RespBody())
require.NoError(t, err)
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
cancel()
resp := respWriter.Result()
// http2RespWriter should rewrite status 101 to 200
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(responseMetaHeaderField))
wg.Wait()
}
func TestServeControlStream(t *testing.T) {
http2Conn, edgeConn := newTestHTTP2Connection()
rpcClientFactory := mockRPCClientFactory{
registered: make(chan struct{}),
unregistered: make(chan struct{}),
}
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
http2Conn.Serve(ctx)
}()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/", nil)
require.NoError(t, err)
req.Header.Set(internalUpgradeHeader, controlStreamUpgrade)
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
require.NoError(t, err)
wg.Add(1)
go func() {
defer wg.Done()
edgeHTTP2Conn.RoundTrip(req)
}()
<-rpcClientFactory.registered
cancel()
<-rpcClientFactory.unregistered
wg.Wait()
}
func benchmarkServeHTTP(b *testing.B, test testRequest) {
http2Conn, edgeConn := newTestHTTP2Connection()
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
http2Conn.Serve(ctx)
}()
endpoint := fmt.Sprintf("http://localhost:8080/%s", test.endpoint)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
require.NoError(b, err)
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StartTimer()
resp, err := edgeHTTP2Conn.RoundTrip(req)
b.StopTimer()
require.NoError(b, err)
require.Equal(b, test.expectedStatus, resp.StatusCode)
if test.expectedBody != nil {
respBody, err := ioutil.ReadAll(resp.Body)
require.NoError(b, err)
require.Equal(b, test.expectedBody, respBody)
}
resp.Body.Close()
}
cancel()
wg.Wait()
}
func BenchmarkServeHTTPSimple(b *testing.B) {
test := testRequest{
name: "ok",
endpoint: "ok",
expectedStatus: http.StatusOK,
expectedBody: []byte(http.StatusText(http.StatusOK)),
}
benchmarkServeHTTP(b, test)
}
func BenchmarkServeHTTPLargeFile(b *testing.B) {
test := testRequest{
name: "large_file",
endpoint: "large_file",
expectedStatus: http.StatusOK,
expectedBody: testLargeResp,
}
benchmarkServeHTTP(b, test)
}
+7
View File
@@ -0,0 +1,7 @@
package connection
import (
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigFastest
+405
View File
@@ -0,0 +1,405 @@
package connection
import (
"sync"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/prometheus/client_golang/prometheus"
)
const (
MetricsNamespace = "cloudflared"
TunnelSubsystem = "tunnel"
muxerSubsystem = "muxer"
)
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 tunnelMetrics struct {
timerRetries prometheus.Gauge
serverLocations *prometheus.GaugeVec
// locationLock is a mutex for oldServerLocations
locationLock sync.Mutex
// oldServerLocations stores the last server the tunnel was connected to
oldServerLocations map[string]string
regSuccess *prometheus.CounterVec
regFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA
userHostnamesCounts *prometheus.CounterVec
}
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 newTunnelMetrics() *tunnelMetrics {
maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: MetricsNamespace,
Subsystem: TunnelSubsystem,
Name: "max_concurrent_requests_per_tunnel",
Help: "Largest number of concurrent requests proxied through each tunnel so far",
},
[]string{"connection_id"},
)
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,
Subsystem: TunnelSubsystem,
Name: "server_locations",
Help: "Where each tunnel is connected to. 1 means current location, 0 means previous locations.",
},
[]string{"connection_id", "location"},
)
prometheus.MustRegister(serverLocations)
rpcFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: TunnelSubsystem,
Name: "tunnel_rpc_fail",
Help: "Count of RPC connection errors by type",
},
[]string{"error", "rpcName"},
)
prometheus.MustRegister(rpcFail)
registerFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: TunnelSubsystem,
Name: "tunnel_register_fail",
Help: "Count of tunnel registration errors by type",
},
[]string{"error", "rpcName"},
)
prometheus.MustRegister(registerFail)
userHostnamesCounts := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: TunnelSubsystem,
Name: "user_hostnames_counts",
Help: "Which user hostnames cloudflared is serving",
},
[]string{"userHostname"},
)
prometheus.MustRegister(userHostnamesCounts)
registerSuccess := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: TunnelSubsystem,
Name: "tunnel_register_success",
Help: "Count of successful tunnel registrations",
},
[]string{"rpcName"},
)
prometheus.MustRegister(registerSuccess)
return &tunnelMetrics{
timerRetries: timerRetries,
serverLocations: serverLocations,
oldServerLocations: make(map[string]string),
muxerMetrics: newMuxerMetrics(),
tunnelsHA: NewTunnelsForHA(),
regSuccess: registerSuccess,
regFail: registerFail,
rpcFail: rpcFail,
userHostnamesCounts: userHostnamesCounts,
}
}
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()
if oldLoc, ok := t.oldServerLocations[connectionID]; ok && oldLoc == loc {
return
} else if ok {
t.serverLocations.WithLabelValues(connectionID, oldLoc).Dec()
}
t.serverLocations.WithLabelValues(connectionID, loc).Inc()
t.oldServerLocations[connectionID] = loc
}
+99
View File
@@ -0,0 +1,99 @@
package connection
import (
"fmt"
"net/url"
"strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
type Observer struct {
logger.Service
metrics *tunnelMetrics
tunnelEventChan chan<- ui.TunnelEvent
}
func NewObserver(logger logger.Service, tunnelEventChan chan<- ui.TunnelEvent) *Observer {
return &Observer{
logger,
newTunnelMetrics(),
tunnelEventChan,
}
}
func (o *Observer) logServerInfo(connIndex uint8, location, msg string) {
// If launch-ui flag is set, send connect msg
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{Index: connIndex, EventType: ui.Connected, Location: location}
}
o.Infof(msg)
o.metrics.registerServerLocation(uint8ToString(connIndex), location)
}
func (o *Observer) logTrialHostname(registration *tunnelpogs.TunnelRegistration) error {
// Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set)
if o.tunnelEventChan == nil {
if registrationURL, err := url.Parse(registration.Url); err == nil {
for _, line := range asciiBox(trialZoneMsg(registrationURL.String()), 2) {
o.Info(line)
}
} else {
o.Error("Failed to connect tunnel, please try again.")
return fmt.Errorf("empty URL in response from Cloudflare edge")
}
}
return nil
}
// Print out the given lines in a nice ASCII box.
func asciiBox(lines []string, padding int) (box []string) {
maxLen := maxLen(lines)
spacer := strings.Repeat(" ", padding)
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
box = append(box, border)
for _, line := range lines {
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
}
box = append(box, border)
return
}
func maxLen(lines []string) int {
max := 0
for _, line := range lines {
if len(line) > max {
max = len(line)
}
}
return max
}
func trialZoneMsg(url string) []string {
return []string{
"Your free tunnel has started! Visit it:",
" " + url,
}
}
func (o *Observer) sendRegisteringEvent() {
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{EventType: ui.RegisteringTunnel}
}
}
func (o *Observer) sendConnectedEvent(connIndex uint8, location string) {
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{Index: connIndex, EventType: ui.Connected, Location: location}
}
}
func (o *Observer) sendURL(url string) {
if o.tunnelEventChan != nil {
o.tunnelEventChan <- ui.TunnelEvent{EventType: ui.SetUrl, Url: url}
}
}
+45
View File
@@ -0,0 +1,45 @@
package connection
import (
"strconv"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
// can only be called once
var m = newTunnelMetrics()
func TestRegisterServerLocation(t *testing.T) {
tunnels := 20
var wg sync.WaitGroup
wg.Add(tunnels)
for i := 0; i < tunnels; i++ {
go func(i int) {
id := strconv.Itoa(i)
m.registerServerLocation(id, "LHR")
wg.Done()
}(i)
}
wg.Wait()
for i := 0; i < tunnels; i++ {
id := strconv.Itoa(i)
assert.Equal(t, "LHR", m.oldServerLocations[id])
}
wg.Add(tunnels)
for i := 0; i < tunnels; i++ {
go func(i int) {
id := strconv.Itoa(i)
m.registerServerLocation(id, "AUS")
wg.Done()
}(i)
}
wg.Wait()
for i := 0; i < tunnels; i++ {
id := strconv.Itoa(i)
assert.Equal(t, "AUS", m.oldServerLocations[id])
}
}
+179
View File
@@ -0,0 +1,179 @@
package connection
import (
"fmt"
"hash/fnv"
"sync"
"time"
"github.com/cloudflare/cloudflared/logger"
)
const (
AvailableProtocolFlagMessage = "Available protocols: http2 - Go's implementation, h2mux - Cloudflare's implementation of HTTP/2, and auto - automatically select between http2 and h2mux"
// edgeH2muxTLSServerName is the server name to establish h2mux connection with edge
edgeH2muxTLSServerName = "cftunnel.com"
// edgeH2TLSServerName is the server name to establish http2 connection with edge
edgeH2TLSServerName = "h2.cftunnel.com"
// threshold to switch back to h2mux when the user intentionally pick --protocol http2
explicitHTTP2FallbackThreshold = -1
autoSelectFlag = "auto"
)
var (
ProtocolList = []Protocol{H2mux, HTTP2}
)
type Protocol int64
const (
H2mux Protocol = iota
HTTP2
)
func (p Protocol) ServerName() string {
switch p {
case H2mux:
return edgeH2muxTLSServerName
case HTTP2:
return edgeH2TLSServerName
default:
return ""
}
}
// Fallback returns the fallback protocol and whether the protocol has a fallback
func (p Protocol) fallback() (Protocol, bool) {
switch p {
case H2mux:
return 0, false
case HTTP2:
return H2mux, true
default:
return 0, false
}
}
func (p Protocol) String() string {
switch p {
case H2mux:
return "h2mux"
case HTTP2:
return "http2"
default:
return fmt.Sprintf("unknown protocol")
}
}
type ProtocolSelector interface {
Current() Protocol
Fallback() (Protocol, bool)
}
type staticProtocolSelector struct {
current Protocol
}
func (s *staticProtocolSelector) Current() Protocol {
return s.current
}
func (s *staticProtocolSelector) Fallback() (Protocol, bool) {
return 0, false
}
type autoProtocolSelector struct {
lock sync.RWMutex
current Protocol
switchThrehold int32
fetchFunc PercentageFetcher
refreshAfter time.Time
ttl time.Duration
logger logger.Service
}
func newAutoProtocolSelector(
current Protocol,
switchThrehold int32,
fetchFunc PercentageFetcher,
ttl time.Duration,
logger logger.Service,
) *autoProtocolSelector {
return &autoProtocolSelector{
current: current,
switchThrehold: switchThrehold,
fetchFunc: fetchFunc,
refreshAfter: time.Now().Add(ttl),
ttl: ttl,
logger: logger,
}
}
func (s *autoProtocolSelector) Current() Protocol {
s.lock.Lock()
defer s.lock.Unlock()
if time.Now().Before(s.refreshAfter) {
return s.current
}
percentage, err := s.fetchFunc()
if err != nil {
s.logger.Errorf("Failed to refresh protocol, err: %v", err)
return s.current
}
if s.switchThrehold < percentage {
s.current = HTTP2
} else {
s.current = H2mux
}
s.refreshAfter = time.Now().Add(s.ttl)
return s.current
}
func (s *autoProtocolSelector) Fallback() (Protocol, bool) {
s.lock.RLock()
defer s.lock.RUnlock()
return s.current.fallback()
}
type PercentageFetcher func() (int32, error)
func NewProtocolSelector(protocolFlag string, namedTunnel *NamedTunnelConfig, fetchFunc PercentageFetcher, ttl time.Duration, logger logger.Service) (ProtocolSelector, error) {
if namedTunnel == nil {
return &staticProtocolSelector{
current: H2mux,
}, nil
}
if protocolFlag == H2mux.String() {
return &staticProtocolSelector{
current: H2mux,
}, nil
}
http2Percentage, err := fetchFunc()
if err != nil {
return nil, err
}
if protocolFlag == HTTP2.String() {
if http2Percentage < 0 {
return newAutoProtocolSelector(H2mux, explicitHTTP2FallbackThreshold, fetchFunc, ttl, logger), nil
}
return newAutoProtocolSelector(HTTP2, explicitHTTP2FallbackThreshold, fetchFunc, ttl, logger), nil
}
if protocolFlag != autoSelectFlag {
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
}
threshold := switchThreshold(namedTunnel.Auth.AccountTag)
if threshold < http2Percentage {
return newAutoProtocolSelector(HTTP2, threshold, fetchFunc, ttl, logger), nil
}
return newAutoProtocolSelector(H2mux, threshold, fetchFunc, ttl, logger), nil
}
func switchThreshold(accountTag string) int32 {
h := fnv.New32a()
h.Write([]byte(accountTag))
return int32(h.Sum32() % 100)
}
+220
View File
@@ -0,0 +1,220 @@
package connection
import (
"fmt"
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/stretchr/testify/assert"
)
const (
testNoTTL = 0
)
var (
testNamedTunnelConfig = &NamedTunnelConfig{
Auth: pogs.TunnelAuth{
AccountTag: "testAccountTag",
},
}
)
func mockFetcher(percentage int32) PercentageFetcher {
return func() (int32, error) {
return percentage, nil
}
}
func mockFetcherWithError() PercentageFetcher {
return func() (int32, error) {
return 0, fmt.Errorf("failed to fetch precentage")
}
}
type dynamicMockFetcher struct {
percentage int32
err error
}
func (dmf *dynamicMockFetcher) fetch() PercentageFetcher {
return func() (int32, error) {
if dmf.err != nil {
return 0, dmf.err
}
return dmf.percentage, nil
}
}
func TestNewProtocolSelector(t *testing.T) {
tests := []struct {
name string
protocol string
expectedProtocol Protocol
hasFallback bool
expectedFallback Protocol
namedTunnelConfig *NamedTunnelConfig
fetchFunc PercentageFetcher
wantErr bool
}{
{
name: "classic tunnel",
protocol: "h2mux",
expectedProtocol: H2mux,
namedTunnelConfig: nil,
},
{
name: "named tunnel over h2mux",
protocol: "h2mux",
expectedProtocol: H2mux,
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "named tunnel over http2",
protocol: "http2",
expectedProtocol: HTTP2,
hasFallback: true,
expectedFallback: H2mux,
fetchFunc: mockFetcher(0),
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "named tunnel http2 disabled",
protocol: "http2",
expectedProtocol: H2mux,
fetchFunc: mockFetcher(-1),
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "named tunnel auto all http2 disabled",
protocol: "auto",
expectedProtocol: H2mux,
fetchFunc: mockFetcher(-1),
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "named tunnel auto to h2mux",
protocol: "auto",
expectedProtocol: H2mux,
fetchFunc: mockFetcher(0),
namedTunnelConfig: testNamedTunnelConfig,
},
{
name: "named tunnel auto to http2",
protocol: "auto",
expectedProtocol: HTTP2,
hasFallback: true,
expectedFallback: H2mux,
fetchFunc: mockFetcher(100),
namedTunnelConfig: testNamedTunnelConfig,
},
{
// None named tunnel can only use h2mux, so specifying an unknown protocol is not an error
name: "classic tunnel unknown protocol",
protocol: "unknown",
expectedProtocol: H2mux,
},
{
name: "named tunnel unknown protocol",
protocol: "unknown",
fetchFunc: mockFetcher(100),
namedTunnelConfig: testNamedTunnelConfig,
wantErr: true,
},
{
name: "named tunnel fetch error",
protocol: "unknown",
fetchFunc: mockFetcherWithError(),
namedTunnelConfig: testNamedTunnelConfig,
wantErr: true,
},
}
logger, _ := logger.New()
for _, test := range tests {
selector, err := NewProtocolSelector(test.protocol, test.namedTunnelConfig, test.fetchFunc, testNoTTL, logger)
if test.wantErr {
assert.Error(t, err, fmt.Sprintf("test %s failed", test.name))
} else {
assert.NoError(t, err, fmt.Sprintf("test %s failed", test.name))
assert.Equal(t, test.expectedProtocol, selector.Current(), fmt.Sprintf("test %s failed", test.name))
fallback, ok := selector.Fallback()
assert.Equal(t, test.hasFallback, ok, fmt.Sprintf("test %s failed", test.name))
if test.hasFallback {
assert.Equal(t, test.expectedFallback, fallback, fmt.Sprintf("test %s failed", test.name))
}
}
}
}
func TestAutoProtocolSelectorRefresh(t *testing.T) {
logger, _ := logger.New()
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, logger)
assert.NoError(t, err)
assert.Equal(t, H2mux, selector.Current())
fetcher.percentage = 100
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = 0
assert.Equal(t, H2mux, selector.Current())
fetcher.percentage = 100
assert.Equal(t, HTTP2, selector.Current())
fetcher.err = fmt.Errorf("failed to fetch")
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = -1
fetcher.err = nil
assert.Equal(t, H2mux, selector.Current())
fetcher.percentage = 0
assert.Equal(t, H2mux, selector.Current())
fetcher.percentage = 100
assert.Equal(t, HTTP2, selector.Current())
}
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
logger, _ := logger.New()
fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector("http2", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, logger)
assert.NoError(t, err)
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = 100
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = 0
assert.Equal(t, HTTP2, selector.Current())
fetcher.err = fmt.Errorf("failed to fetch")
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = -1
fetcher.err = nil
assert.Equal(t, H2mux, selector.Current())
fetcher.percentage = 0
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = 100
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = -1
assert.Equal(t, H2mux, selector.Current())
}
func TestProtocolSelectorRefreshTTL(t *testing.T) {
logger, _ := logger.New()
fetcher := dynamicMockFetcher{percentage: 100}
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), time.Hour, logger)
assert.NoError(t, err)
assert.Equal(t, HTTP2, selector.Current())
fetcher.percentage = 0
assert.Equal(t, HTTP2, selector.Current())
}
+275 -29
View File
@@ -3,47 +3,293 @@ package connection
import (
"context"
"fmt"
"io"
"time"
rpc "zombiezen.com/go/capnproto2/rpc"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"zombiezen.com/go/capnproto2/rpc"
)
// NewRPCClient creates and returns a new RPC client, which will communicate
// using a stream on the given muxer
func NewRPCClient(
type tunnelServerClient struct {
client tunnelpogs.TunnelServer_PogsClient
transport rpc.Transport
}
// NewTunnelRPCClient creates and returns a new RPC client, which will communicate using a stream on the given muxer.
// This method is exported for supervisor to call Authenticate RPC
func NewTunnelServerClient(
ctx context.Context,
muxer *h2mux.Muxer,
stream io.ReadWriteCloser,
logger logger.Service,
openStreamTimeout time.Duration,
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)
defer openStreamCancel()
stream, err := muxer.OpenRPCStream(openStreamCtx)
) *tunnelServerClient {
transport := tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream))
conn := rpc.NewConn(
transport,
tunnelrpc.ConnLog(logger),
)
registrationClient := tunnelpogs.RegistrationServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
return &tunnelServerClient{
client: tunnelpogs.TunnelServer_PogsClient{RegistrationServer_PogsClient: registrationClient, Client: conn.Bootstrap(ctx), Conn: conn},
transport: transport,
}
}
func (tsc *tunnelServerClient) Authenticate(ctx context.Context, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) (tunnelpogs.AuthOutcome, error) {
authResp, err := tsc.client.Authenticate(ctx, classicTunnel.OriginCert, classicTunnel.Hostname, registrationOptions)
if err != nil {
return nil, err
}
return authResp.Outcome(), nil
}
func (tsc *tunnelServerClient) Close() {
// Closing the client will also close the connection
tsc.client.Close()
tsc.transport.Close()
}
type NamedTunnelRPCClient interface {
RegisterConnection(
c context.Context,
config *NamedTunnelConfig,
options *tunnelpogs.ConnectionOptions,
connIndex uint8,
observer *Observer,
) error
GracefulShutdown(ctx context.Context, gracePeriod time.Duration)
Close()
}
type registrationServerClient struct {
client tunnelpogs.RegistrationServer_PogsClient
transport rpc.Transport
}
func newRegistrationRPCClient(
ctx context.Context,
stream io.ReadWriteCloser,
logger logger.Service,
) NamedTunnelRPCClient {
transport := tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream))
conn := rpc.NewConn(
transport,
tunnelrpc.ConnLog(logger),
)
return &registrationServerClient{
client: tunnelpogs.RegistrationServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn},
transport: transport,
}
}
func (rsc *registrationServerClient) RegisterConnection(
ctx context.Context,
config *NamedTunnelConfig,
options *tunnelpogs.ConnectionOptions,
connIndex uint8,
observer *Observer,
) error {
conn, err := rsc.client.RegisterConnection(
ctx,
config.Auth,
config.ID,
connIndex,
options,
)
if err != nil {
if err.Error() == DuplicateConnectionError {
observer.metrics.regFail.WithLabelValues("dup_edge_conn", "registerConnection").Inc()
return errDuplicationConnection
}
observer.metrics.regFail.WithLabelValues("server_error", "registerConnection").Inc()
return serverRegistrationErrorFromRPC(err)
}
observer.metrics.regSuccess.WithLabelValues("registerConnection").Inc()
observer.logServerInfo(connIndex, conn.Location, fmt.Sprintf("Connection %d registered with %s using ID %s", connIndex, conn.Location, conn.UUID))
observer.sendConnectedEvent(connIndex, conn.Location)
return nil
}
func (rsc *registrationServerClient) GracefulShutdown(ctx context.Context, gracePeriod time.Duration) {
ctx, cancel := context.WithTimeout(ctx, gracePeriod)
defer cancel()
rsc.client.UnregisterConnection(ctx)
}
func (rsc *registrationServerClient) Close() {
// Closing the client will also close the connection
rsc.client.Close()
// Closing the transport also closes the stream
rsc.transport.Close()
}
type rpcName string
const (
register rpcName = "register"
reconnect rpcName = "reconnect"
unregister rpcName = "unregister"
authenticate rpcName = " authenticate"
)
func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter CredentialManager, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) error {
h.observer.sendRegisteringEvent()
stream, err := h.newRPCStream(ctx, register)
if err != nil {
return err
}
rpcClient := NewTunnelServerClient(ctx, stream, h.observer)
defer rpcClient.Close()
h.logServerInfo(ctx, rpcClient)
registration := rpcClient.client.RegisterTunnel(
ctx,
classicTunnel.OriginCert,
classicTunnel.Hostname,
registrationOptions,
)
if registrationErr := registration.DeserializeError(); registrationErr != nil {
// RegisterTunnel RPC failure
return h.processRegisterTunnelError(registrationErr, register)
}
// Send free tunnel URL to UI
h.observer.sendURL(registration.Url)
credentialSetter.SetEventDigest(h.connIndex, registration.EventDigest)
return h.processRegistrationSuccess(registration, register, credentialSetter, classicTunnel)
}
type CredentialManager interface {
ReconnectToken() ([]byte, error)
EventDigest(connID uint8) ([]byte, error)
SetEventDigest(connID uint8, digest []byte)
ConnDigest(connID uint8) ([]byte, error)
SetConnDigest(connID uint8, digest []byte)
}
func (h *h2muxConnection) processRegistrationSuccess(
registration *tunnelpogs.TunnelRegistration,
name rpcName,
credentialManager CredentialManager, classicTunnel *ClassicTunnelConfig,
) error {
for _, logLine := range registration.LogLines {
h.observer.Info(logLine)
}
if registration.TunnelID != "" {
h.observer.metrics.tunnelsHA.AddTunnelID(h.connIndex, registration.TunnelID)
h.observer.Infof("Each HA connection's tunnel IDs: %v", h.observer.metrics.tunnelsHA.String())
}
// Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set)
if classicTunnel.IsTrialZone() {
err := h.observer.logTrialHostname(registration)
if err != nil {
return err
}
}
credentialManager.SetConnDigest(h.connIndex, registration.ConnDigest)
h.observer.metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
h.observer.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
h.observer.metrics.regSuccess.WithLabelValues(string(name)).Inc()
return nil
}
func (h *h2muxConnection) processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, name rpcName) error {
if err.Error() == DuplicateConnectionError {
h.observer.metrics.regFail.WithLabelValues("dup_edge_conn", string(name)).Inc()
return errDuplicationConnection
}
h.observer.metrics.regFail.WithLabelValues("server_error", string(name)).Inc()
return serverRegisterTunnelError{
cause: err,
permanent: err.IsPermanent(),
}
}
func (h *h2muxConnection) reconnectTunnel(ctx context.Context, credentialManager CredentialManager, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) error {
token, err := credentialManager.ReconnectToken()
if err != nil {
return err
}
eventDigest, err := credentialManager.EventDigest(h.connIndex)
if err != nil {
return err
}
connDigest, err := credentialManager.ConnDigest(h.connIndex)
if err != nil {
return err
}
h.observer.Debug("initiating RPC stream to reconnect")
stream, err := h.newRPCStream(ctx, register)
if err != nil {
return err
}
rpcClient := NewTunnelServerClient(ctx, stream, h.observer)
defer rpcClient.Close()
h.logServerInfo(ctx, rpcClient)
registration := rpcClient.client.ReconnectTunnel(
ctx,
token,
eventDigest,
connDigest,
classicTunnel.Hostname,
registrationOptions,
)
if registrationErr := registration.DeserializeError(); registrationErr != nil {
// ReconnectTunnel RPC failure
return h.processRegisterTunnelError(registrationErr, reconnect)
}
return h.processRegistrationSuccess(registration, reconnect, credentialManager, classicTunnel)
}
func (h *h2muxConnection) logServerInfo(ctx context.Context, rpcClient *tunnelServerClient) error {
// Request server info without blocking tunnel registration; must use capnp library directly.
serverInfoPromise := tunnelrpc.TunnelServer{Client: rpcClient.client.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
serverInfoMessage, err := serverInfoPromise.Result().Struct()
if err != nil {
h.observer.Errorf("Failed to retrieve server information: %s", err)
return err
}
serverInfo, err := tunnelpogs.UnmarshalServerInfo(serverInfoMessage)
if err != nil {
h.observer.Errorf("Failed to retrieve server information: %s", err)
return err
}
h.observer.logServerInfo(h.connIndex, serverInfo.LocationName, fmt.Sprintf("Connnection %d connected to %s", h.connIndex, serverInfo.LocationName))
return nil
}
func (h *h2muxConnection) unregister(isNamedTunnel bool) {
unregisterCtx, cancel := context.WithTimeout(context.Background(), h.config.GracePeriod)
defer cancel()
stream, err := h.newRPCStream(unregisterCtx, register)
if err != nil {
return
}
if !isRPCStreamResponse(stream.Headers) {
stream.Close()
err = fmt.Errorf("rpc: bad response headers: %v", stream.Headers)
return
if isNamedTunnel {
rpcClient := newRegistrationRPCClient(unregisterCtx, stream, h.observer)
defer rpcClient.Close()
rpcClient.GracefulShutdown(unregisterCtx, h.config.GracePeriod)
} else {
rpcClient := NewTunnelServerClient(unregisterCtx, stream, h.observer)
defer rpcClient.Close()
// gracePeriod is encoded in int64 using capnproto
rpcClient.client.UnregisterTunnel(unregisterCtx, h.config.GracePeriod.Nanoseconds())
}
conn := rpc.NewConn(
tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream)),
tunnelrpc.ConnLog(logger),
)
client = tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}
return client, nil
}
func isRPCStreamResponse(headers []h2mux.Header) bool {
return len(headers) == 1 &&
headers[0].Name == ":status" &&
headers[0].Value == "200"
}
+50
View File
@@ -0,0 +1,50 @@
package connection
import (
"fmt"
"sync"
"github.com/prometheus/client_golang/prometheus"
)
// tunnelsForHA maps this cloudflared instance's HA connections to the tunnel IDs they serve.
type tunnelsForHA struct {
sync.Mutex
metrics *prometheus.GaugeVec
entries map[uint8]string
}
// NewTunnelsForHA initializes the Prometheus metrics etc for a tunnelsForHA.
func NewTunnelsForHA() tunnelsForHA {
metrics := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "tunnel_ids",
Help: "The ID of all tunnels (and their corresponding HA connection ID) running in this instance of cloudflared.",
},
[]string{"tunnel_id", "ha_conn_id"},
)
prometheus.MustRegister(metrics)
return tunnelsForHA{
metrics: metrics,
entries: make(map[uint8]string),
}
}
// Track a new tunnel ID, removing the disconnected tunnel (if any) and update metrics.
func (t *tunnelsForHA) AddTunnelID(haConn uint8, tunnelID string) {
t.Lock()
defer t.Unlock()
haStr := fmt.Sprintf("%v", haConn)
if oldTunnelID, ok := t.entries[haConn]; ok {
t.metrics.WithLabelValues(oldTunnelID, haStr).Dec()
}
t.entries[haConn] = tunnelID
t.metrics.WithLabelValues(tunnelID, haStr).Inc()
}
func (t *tunnelsForHA) String() string {
t.Lock()
defer t.Unlock()
return fmt.Sprintf("%v", t.entries)
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"net"
"strconv"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
// Cmd is the entrypoint command for dbconnect.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
func TestCmd(t *testing.T) {
+6 -1
View File
@@ -1,4 +1,9 @@
FROM golang:1.12 as builder
FROM golang:1.15.3 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0
WORKDIR /go/src/github.com/cloudflare/cloudflared/
RUN apt-get update
COPY . .
# compile cloudflared
RUN make cloudflared
RUN cp /go/src/github.com/cloudflare/cloudflared/cloudflared /usr/local/bin/
+6 -6
View File
@@ -121,15 +121,15 @@ func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
}
// ResolveAddrs resolves TCP address given a list of addresses. Address can be a hostname, however, it will return at most one
// of the hostname's IP addresses
func ResolveAddrs(addrs []string) ([]*net.TCPAddr, error) {
var tcpAddrs []*net.TCPAddr
// of the hostname's IP addresses.
func ResolveAddrs(addrs []string, logger logger.Service) (resolved []*net.TCPAddr) {
for _, addr := range addrs {
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return nil, err
logger.Errorf("Failed to resolve %s, err: %v", addr, err)
} else {
resolved = append(resolved, tcpAddr)
}
tcpAddrs = append(tcpAddrs, tcpAddr)
}
return tcpAddrs, nil
return
}
+5 -5
View File
@@ -35,12 +35,12 @@ func ResolveEdge(logger logger.Service) (*Regions, error) {
// StaticEdge creates a list of edge addresses from the list of hostnames.
// Mainly used for testing connectivity.
func StaticEdge(hostnames []string) (*Regions, error) {
addrs, err := ResolveAddrs(hostnames)
if err != nil {
return nil, err
func StaticEdge(hostnames []string, logger logger.Service) (*Regions, error) {
resolved := ResolveAddrs(hostnames, logger)
if len(resolved) == 0 {
return nil, fmt.Errorf("failed to resolve any edge address")
}
return NewNoResolve(addrs), nil
return NewNoResolve(resolved), nil
}
// NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses.
+3 -2
View File
@@ -1,4 +1,4 @@
package connection
package edgediscovery
import (
"context"
@@ -9,7 +9,7 @@ import (
"github.com/pkg/errors"
)
// DialEdge makes a TLS connection to a Cloudflare edge node
// DialEdgeWithH2Mux makes a TLS connection to a Cloudflare edge node
func DialEdge(
ctx context.Context,
timeout time.Duration,
@@ -25,6 +25,7 @@ func DialEdge(
if err != nil {
return nil, newDialError(err, "DialContext error")
}
tlsEdgeConn := tls.Client(edgeConn, tlsConfig)
tlsEdgeConn.SetDeadline(time.Now().Add(timeout))
+1 -1
View File
@@ -41,7 +41,7 @@ func ResolveEdge(l logger.Service) (*Edge, error) {
// StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity.
func StaticEdge(l logger.Service, hostnames []string) (*Edge, error) {
regions, err := allregions.StaticEdge(hostnames)
regions, err := allregions.StaticEdge(hostnames, l)
if err != nil {
return new(Edge), err
}
@@ -1,4 +1,4 @@
package connection
package edgediscovery
import (
"fmt"
+45
View File
@@ -0,0 +1,45 @@
package edgediscovery
import (
"fmt"
"net"
"strconv"
"strings"
)
const (
protocolRecord = "protocol.argotunnel.com"
)
var (
errNoProtocolRecord = fmt.Errorf("No TXT record found for %s to determine connection protocol", protocolRecord)
)
func HTTP2Percentage() (int32, error) {
records, err := net.LookupTXT(protocolRecord)
if err != nil {
return 0, err
}
if len(records) == 0 {
return 0, errNoProtocolRecord
}
return parseHTTP2Precentage(records[0])
}
// The record looks like http2=percentage
func parseHTTP2Precentage(record string) (int32, error) {
const key = "http2"
slices := strings.Split(record, "=")
if len(slices) != 2 {
return 0, fmt.Errorf("Malformed TXT record %s, expect http2=percentage", record)
}
if slices[0] != key {
return 0, fmt.Errorf("Incorrect key %s, expect %s", slices[0], key)
}
percentage, err := strconv.ParseInt(slices[1], 10, 32)
if err != nil {
return 0, err
}
return int32(percentage), nil
}
+80
View File
@@ -0,0 +1,80 @@
package edgediscovery
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHTTP2Percentage(t *testing.T) {
_, err := HTTP2Percentage()
assert.NoError(t, err)
}
func TestParseHTTP2Precentage(t *testing.T) {
tests := []struct {
record string
percentage int32
wantErr bool
}{
{
record: "http2=-1",
percentage: -1,
wantErr: false,
},
{
record: "http2=0",
percentage: 0,
wantErr: false,
},
{
record: "http2=50",
percentage: 50,
wantErr: false,
},
{
record: "http2=100",
percentage: 100,
wantErr: false,
},
{
record: "http2=1000",
percentage: 1000,
wantErr: false,
},
{
record: "http2=10.5",
wantErr: true,
},
{
record: "http2=10 h2mux=90",
wantErr: true,
},
{
record: "http2=ten",
wantErr: true,
},
{
record: "h2mux=100",
wantErr: true,
},
{
record: "http2",
wantErr: true,
},
{
record: "http2=",
wantErr: true,
},
}
for _, test := range tests {
p, err := parseHTTP2Precentage(test.record)
if test.wantErr {
assert.Error(t, err)
} else {
assert.Equal(t, test.percentage, p)
}
}
}
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/python3
"""
Creates Github Releases Notes with content hashes
"""
import argparse
import logging
import os
import hashlib
import glob
from github import Github, GithubException, UnknownObjectException
FORMAT = "%(levelname)s - %(asctime)s: %(message)s"
logging.basicConfig(format=FORMAT)
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
GITHUB_CONFLICT_CODE = "already_exists"
def get_sha256(filename):
""" get the sha256 of a file """
sha256_hash = hashlib.sha256()
with open(filename,"rb") as f:
for byte_block in iter(lambda: f.read(4096),b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def update_or_add_message(msg, name, sha):
"""
updates or builds the github version message for each new asset's sha256.
Searches the existing message string to update or create.
"""
new_text = '{0}: {1}\n'.format(name, sha)
start = msg.find(name)
if (start != -1):
end = msg.find("\n", start)
if (end != -1):
return msg.replace(msg[start:end+1], new_text)
back = msg.rfind("```")
if (back != -1):
return '{0}{1}```'.format(msg[:back], new_text)
return '{0} \n### SHA256 Checksums:\n```\n{1}```'.format(msg, new_text)
def get_release(repo, version):
""" Get a Github Release matching the version tag. """
try:
release = repo.get_release(version)
logging.info("Release %s found", version)
return release
except UnknownObjectException:
logging.info("Release %s not found", version)
def parse_args():
""" Parse and validate args """
parser = argparse.ArgumentParser(
description="Creates Github Releases and uploads assets."
)
parser.add_argument(
"--api-key", default=os.environ.get("API_KEY"), help="Github API key"
)
parser.add_argument(
"--release-version",
metavar="version",
default=os.environ.get("VERSION"),
help="Release version",
)
parser.add_argument(
"--dry-run", action="store_true", help="Do not modify the release message"
)
args = parser.parse_args()
is_valid = True
if not args.release_version:
logging.error("Missing release version")
is_valid = False
if not args.api_key:
logging.error("Missing API key")
is_valid = False
if is_valid:
return args
parser.print_usage()
exit(1)
def main():
""" Attempts to update the Github Release message with the github asset's checksums """
try:
args = parse_args()
client = Github(args.api_key)
repo = client.get_repo(CLOUDFLARED_REPO)
release = get_release(repo, args.release_version)
msg = release.body
for filepath in glob.glob("artifacts/*"):
pkg_hash = get_sha256(filepath)
# add the sha256 of the new artifact to the release message body
name = os.path.basename(filepath)
msg = update_or_add_message(msg, name, pkg_hash)
if args.dry_run:
logging.info("Skipping asset upload because of dry-run")
return
# update the release body text
release.update_release(args.release_version, msg)
except Exception as e:
logging.exception(e)
exit(1)
main()
+85
View File
@@ -6,6 +6,10 @@ Creates Github Releases and uploads assets
import argparse
import logging
import os
import shutil
import hashlib
import requests
import tarfile
from github import Github, GithubException, UnknownObjectException
@@ -14,6 +18,36 @@ logging.basicConfig(format=FORMAT)
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
GITHUB_CONFLICT_CODE = "already_exists"
BASE_KV_URL = 'https://api.cloudflare.com/client/v4/accounts/'
UPDATER_PREFIX = 'update'
def get_sha256(filename):
""" get the sha256 of a file """
sha256_hash = hashlib.sha256()
with open(filename,"rb") as f:
for byte_block in iter(lambda: f.read(4096),b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def send_hash(pkg_hash, name, version, account, namespace, api_token):
""" send the checksum of a file to workers kv """
key = '{0}_{1}_{2}'.format(UPDATER_PREFIX, version, name)
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + api_token,
}
response = requests.put(
BASE_KV_URL + account + "/storage/kv/namespaces/" + namespace + "/values/" + key,
headers=headers,
data=pkg_hash
)
if response.status_code != 200:
jsonResponse = response.json()
errors = jsonResponse["errors"]
if len(errors) > 0:
raise Exception("failed to upload checksum: {0}", errors[0])
def assert_tag_exists(repo, version):
@@ -78,6 +112,15 @@ def parse_args():
parser.add_argument(
"--name", default=os.environ.get("ASSET_NAME"), help="Asset Name"
)
parser.add_argument(
"--namespace-id", default=os.environ.get("KV_NAMESPACE"), help="workersKV namespace id"
)
parser.add_argument(
"--kv-account-id", default=os.environ.get("KV_ACCOUNT"), help="workersKV account id"
)
parser.add_argument(
"--kv-api-token", default=os.environ.get("KV_API_TOKEN"), help="workersKV API Token"
)
parser.add_argument(
"--dry-run", action="store_true", help="Do not create release or upload asset"
)
@@ -99,6 +142,18 @@ def parse_args():
if not args.api_key:
logging.error("Missing API key")
is_valid = False
if not args.namespace_id:
logging.error("Missing KV namespace id")
is_valid = False
if not args.kv_account_id:
logging.error("Missing KV account id")
is_valid = False
if not args.kv_api_token:
logging.error("Missing KV API token")
is_valid = False
if is_valid:
return args
@@ -120,6 +175,36 @@ def main():
return
release.upload_asset(args.path, name=args.name)
# check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
binary_path = args.path
if binary_path.endswith("tgz"):
try:
shutil.rmtree('cfd')
except OSError as e:
pass
zipfile = tarfile.open(binary_path, "r:gz")
zipfile.extractall('cfd') # specify which folder to extract to
zipfile.close()
binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared')
# send the sha256 (the checksum) to workers kv
pkg_hash = get_sha256(binary_path)
send_hash(pkg_hash, args.name, args.release_version, args.kv_account_id, args.namespace_id, args.kv_api_token)
# create the artifacts directory if it doesn't exist
artifact_path = os.path.join(os.getcwd(), 'artifacts')
if not os.path.isdir(artifact_path):
os.mkdir(artifact_path)
# copy the binary to the path
copy_path = os.path.join(artifact_path, args.name)
try:
shutil.copy(args.path, copy_path)
except shutil.SameFileError:
pass # the macOS release copy fails with being the same file (already in the artifacts directory). Catching to ignore.
except Exception as e:
logging.exception(e)
exit(1)
+37 -43
View File
@@ -1,78 +1,72 @@
module github.com/cloudflare/cloudflared
go 1.12
go 1.15
require (
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc // indirect
github.com/DATA-DOG/go-sqlmock v1.3.3
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/aws/aws-sdk-go v1.25.8
github.com/beorn7/perks v1.0.1 // indirect
github.com/aws/aws-sdk-go v1.34.19
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2 // indirect
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
github.com/coredns/coredns v1.2.0
github.com/coredns/coredns v1.7.0
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0
github.com/equinox-io/equinox v1.2.0
github.com/equinox-io/equinox v1.2.0 // indirect
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect
github.com/facebookgo/grace v0.0.0-20180706040059-75cf19382434
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/frankban/quicktest v1.10.0 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/gdamore/tcell v1.3.0
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0
github.com/go-sql-driver/mysql v1.4.1
github.com/go-sql-driver/mysql v1.5.0
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.0.4
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/google/uuid v1.1.1
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/uuid v1.1.2
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/gorilla/websocket v1.4.2
github.com/jmoiron/sqlx v1.2.0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/json-iterator/go v1.1.10
github.com/kr/text v0.2.0 // indirect
github.com/kshvakov/clickhouse v1.3.11
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.2.0
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-sqlite3 v1.11.0
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b // indirect
github.com/miekg/dns v1.1.27
github.com/miekg/dns v1.1.31
github.com/mitchellh/go-homedir v1.1.0
github.com/opentracing/opentracing-go v1.1.0 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
github.com/pkg/errors v0.8.1
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v1.0.0
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
github.com/prometheus/common v0.7.0 // indirect
github.com/prometheus/procfs v0.0.5 // indirect
github.com/prometheus/client_golang v1.7.1
github.com/prometheus/common v0.13.0 // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
github.com/stretchr/testify v1.3.0
github.com/tinylib/msgp v1.1.0 // indirect
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92
github.com/stretchr/testify v1.6.0
github.com/urfave/cli/v2 v2.2.0
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
google.golang.org/appengine v1.5.0 // indirect
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect
google.golang.org/grpc v1.24.0 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20200904194848-62affa334b73
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
google.golang.org/grpc v1.32.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/coreos/go-oidc.v2 v2.1.0
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
zombiezen.com/go/capnproto2 v2.18.0+incompatible
)
// ../../go/pkg/mod/github.com/coredns/coredns@v1.2.0/plugin/metrics/metrics.go:40:49: too many arguments in call to prometheus.NewProcessCollector
// have (int, string)
// want (prometheus.ProcessCollectorOpts)
replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v0.9.0-pre1
+844 -119
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -7,6 +7,19 @@ import (
"golang.org/x/net/http2"
)
var (
ActiveStreams = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "cloudflared",
Subsystem: "tunnel",
Name: "active_streams",
Help: "Number of active streams created by all muxers.",
})
)
func init() {
prometheus.MustRegister(ActiveStreams)
}
// activeStreamMap is used to moderate access to active streams between the read and write
// threads, and deny access to new peer streams while shutting down.
type activeStreamMap struct {
+3 -3
View File
@@ -9,7 +9,7 @@ import (
func TestShutdown(t *testing.T) {
const numStreams = 1000
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
m := newActiveStreamMap(true, ActiveStreams)
// Add all the streams
{
@@ -62,7 +62,7 @@ func TestShutdown(t *testing.T) {
func TestEmptyBeforeShutdown(t *testing.T) {
const numStreams = 1000
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
m := newActiveStreamMap(true, ActiveStreams)
// Add all the streams
{
@@ -138,7 +138,7 @@ func (_ *noopReadyList) Signal(streamID uint32) {}
func TestAbort(t *testing.T) {
const numStreams = 1000
m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name()))
m := newActiveStreamMap(true, ActiveStreams)
var openedStreams sync.Map
+1
View File
@@ -19,6 +19,7 @@ var (
ErrUnexpectedFrameType = MuxerProtocolError{"2001 unexpected frame type", http2.ErrCodeProtocol}
ErrUnknownStream = MuxerProtocolError{"2002 unknown stream", http2.ErrCodeProtocol}
ErrInvalidStream = MuxerProtocolError{"2003 invalid stream", http2.ErrCodeProtocol}
ErrNotRPCStream = MuxerProtocolError{"2004 not RPC stream", http2.ErrCodeProtocol}
ErrStreamHeadersSent = MuxerApplicationError{"3000 headers already sent"}
ErrStreamRequestConnectionClosed = MuxerApplicationError{"3001 connection closed while opening stream"}
+19 -4
View File
@@ -7,11 +7,12 @@ import (
"sync"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/logger"
)
const (
@@ -21,6 +22,7 @@ const (
defaultTimeout time.Duration = 5 * time.Second
defaultRetries uint64 = 5
defaultWriteBufferMaxLen int = 1024 * 1024 // 1mb
writeBufferInitialSize int = 16 * 1024 // 16KB
SettingMuxerMagic http2.SettingID = 0x42db
MuxerMagicOrigin uint32 = 0xa2e43c8b
@@ -206,9 +208,9 @@ func Handshake(
initialStreamWindow: m.config.DefaultWindowSize,
streamWindowMax: m.config.MaxWindowSize,
streamWriteBufferMaxLen: m.config.StreamWriteBufferMaxLen,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
}
m.muxWriter = &MuxWriter{
f: m.f,
@@ -439,11 +441,17 @@ func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader
func (m *Muxer) OpenRPCStream(ctx context.Context) (*MuxedStream, error) {
stream := m.NewStream(RPCHeaders())
if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, nil)); err != nil {
stream.Close()
return nil, err
}
if err := m.AwaitResponseHeaders(ctx, stream); err != nil {
stream.Close()
return nil, err
}
if !IsRPCStreamResponse(stream) {
stream.Close()
return nil, ErrNotRPCStream
}
return stream, nil
}
@@ -497,3 +505,10 @@ func (m *Muxer) abort() {
func (m *Muxer) TimerRetries() uint64 {
return m.muxWriter.idleTimer.RetryCount()
}
func IsRPCStreamResponse(stream *MuxedStream) bool {
headers := stream.Headers
return len(headers) == 1 &&
headers[0].Name == ":status" &&
headers[0].Value == "200"
}
+109 -3
View File
@@ -15,10 +15,11 @@ import (
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/logger"
)
const (
@@ -112,11 +113,11 @@ func (p *DefaultMuxerPair) Handshake(testName string) error {
defer cancel()
errGroup, _ := errgroup.WithContext(ctx)
errGroup.Go(func() (err error) {
p.EdgeMux, err = Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig, NewActiveStreamsMetrics(testName, "edge"))
p.EdgeMux, err = Handshake(p.EdgeConn, p.EdgeConn, p.EdgeMuxConfig, ActiveStreams)
return errors.Wrap(err, "edge handshake failure")
})
errGroup.Go(func() (err error) {
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig, NewActiveStreamsMetrics(testName, "origin"))
p.OriginMux, err = Handshake(p.OriginConn, p.OriginConn, p.OriginMuxConfig, ActiveStreams)
return errors.Wrap(err, "origin handshake failure")
})
@@ -1032,3 +1033,108 @@ func openStreams(b *testing.B, muxPair *DefaultMuxerPair, n int) {
}
assert.NoError(b, errGroup.Wait())
}
func BenchmarkSingleStreamLargeResponseBody(b *testing.B) {
const bodySize = 1 << 24
const writeBufferSize = 16 << 10
const writeN = bodySize / writeBufferSize
payload := make([]byte, writeBufferSize)
for i := range payload {
payload[i] = byte(i % 256)
}
const readBufferSize = 16 << 10
const readN = bodySize / readBufferSize
responseBody := make([]byte, readBufferSize)
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
if stream.Headers[0].Name != "test-header" {
b.Fatalf("expected header name %s, got %s", "test-header", stream.Headers[0].Name)
}
if stream.Headers[0].Value != "headerValue" {
b.Fatalf("expected header value %s, got %s", "headerValue", stream.Headers[0].Value)
}
stream.WriteHeaders([]Header{
{Name: "response-header", Value: "responseValue"},
})
for i := 0; i < writeN; i++ {
n, err := stream.Write(payload)
if err != nil {
b.Fatalf("origin write error: %s", err)
}
if n != len(payload) {
b.Fatalf("origin short write: %d/%d bytes", n, len(payload))
}
}
return nil
})
name := fmt.Sprintf("%s_%d", b.Name(), rand.Int())
origin, edge := net.Pipe()
muxPair := &DefaultMuxerPair{
OriginMuxConfig: MuxerConfig{
Timeout: testHandshakeTimeout,
Handler: f,
IsClient: true,
Name: "origin",
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: defaultWindowSize,
MaxWindowSize: maxWindowSize,
StreamWriteBufferMaxLen: defaultWriteBufferMaxLen,
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
OriginConn: origin,
EdgeMuxConfig: MuxerConfig{
Timeout: testHandshakeTimeout,
IsClient: false,
Name: "edge",
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: defaultWindowSize,
MaxWindowSize: maxWindowSize,
StreamWriteBufferMaxLen: defaultWriteBufferMaxLen,
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
EdgeConn: edge,
doneC: make(chan struct{}),
}
assert.NoError(b, muxPair.Handshake(name))
muxPair.Serve(b)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
stream, err := muxPair.OpenEdgeMuxStream(
[]Header{{Name: "test-header", Value: "headerValue"}},
nil,
)
if err != nil {
b.Fatalf("error in OpenStream: %s", err)
}
if len(stream.Headers) != 1 {
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
}
if stream.Headers[0].Name != "response-header" {
b.Fatalf("expected header name %s, got %s", "response-header", stream.Headers[0].Name)
}
if stream.Headers[0].Value != "responseValue" {
b.Fatalf("expected header value %s, got %s", "responseValue", stream.Headers[0].Value)
}
for k := 0; k < readN; k++ {
n, err := io.ReadFull(stream, responseBody)
if err != nil {
b.Fatalf("error from (*MuxedStream).Read: %s", err)
}
if n != len(responseBody) {
b.Fatalf("expected response body to have %d bytes, got %d", len(responseBody), n)
}
}
}
}
+51 -71
View File
@@ -2,7 +2,6 @@ package h2mux
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
@@ -22,10 +21,6 @@ const (
RequestUserHeadersField = "cf-cloudflared-request-headers"
ResponseUserHeadersField = "cf-cloudflared-response-headers"
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
ResponseSourceCloudflared = "cloudflared"
ResponseSourceOrigin = "origin"
CFAccessTokenHeader = "cf-access-token"
CFJumpDestinationHeader = "CF-Access-Jump-Destination"
CFAccessClientIDHeader = "CF-Access-Client-Id"
@@ -38,11 +33,12 @@ const (
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
for _, header := range h2 {
if !IsControlHeader(header.Name) {
name := strings.ToLower(header.Name)
if !IsControlHeader(name) {
continue
}
switch strings.ToLower(header.Name) {
switch name {
case ":method":
h1.Method = header.Value
case ":scheme":
@@ -116,18 +112,14 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header,
}
func IsControlHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-")
}
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
func IsWebsocketClientHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "sec-websocket-accept" ||
headerName == "connection" ||
headerName == "upgrade"
@@ -137,56 +129,72 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
h2 = []Header{
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
}
userHeaders := http.Header{}
userHeaders := make(http.Header, len(h1.Header))
for header, values := range h1.Header {
for _, value := range values {
if strings.ToLower(header) == "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.
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, Header{Name: strings.ToLower(header), Value: value})
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
// 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
if _, ok := userHeaders[header]; ok {
userHeaders[header] = append(userHeaders[header], value)
} else {
userHeaders[header] = []string{value}
}
}
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, Header{Name: "content-length", Value: values[0]})
} else if !IsControlHeader(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, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...)
h2 = append(h2, Header{ResponseUserHeadersField, SerializeHeaders(userHeaders)})
return h2
}
// Serialize HTTP1.x headers by base64-encoding each header name and value,
// and then joining them in the format of [key:value;]
func SerializeHeaders(h1Headers http.Header) string {
var serializedHeaders []string
// compute size of the fully serialized value and largest temp buffer we will need
serializedLen := 0
maxTempLen := 0
for headerName, headerValues := range h1Headers {
for _, headerValue := range headerValues {
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
headerEncoding.Encode(encodedName, []byte(headerName))
nameLen := headerEncoding.EncodedLen(len(headerName))
valueLen := headerEncoding.EncodedLen(len(headerValue))
const delims = 2
serializedLen += delims + nameLen + valueLen
if nameLen > maxTempLen {
maxTempLen = nameLen
}
if valueLen > maxTempLen {
maxTempLen = valueLen
}
}
}
var buf strings.Builder
buf.Grow(serializedLen)
encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue)))
headerEncoding.Encode(encodedValue, []byte(headerValue))
temp := make([]byte, maxTempLen)
writeB64 := func(s string) {
n := headerEncoding.EncodedLen(len(s))
if n > len(temp) {
temp = make([]byte, n)
}
headerEncoding.Encode(temp[:n], []byte(s))
buf.Write(temp[:n])
}
serializedHeaders = append(
serializedHeaders,
strings.Join(
[]string{string(encodedName), string(encodedValue)},
":",
),
)
for headerName, headerValues := range h1Headers {
for _, headerValue := range headerValues {
if buf.Len() > 0 {
buf.WriteByte(';')
}
writeB64(headerName)
buf.WriteByte(':')
writeB64(headerValue)
}
}
return strings.Join(serializedHeaders, ";")
return buf.String()
}
// Deserialize headers serialized by `SerializeHeader`
@@ -224,31 +232,3 @@ func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
return deserialized, nil
}
func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header {
var serializedHeaderChunks []string
for _, headerChunk := range headers {
serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk))
}
return []Header{{
headersField,
strings.Join(serializedHeaderChunks, ";"),
}}
}
type ResponseMetaHeader struct {
Source string `json:"src"`
}
func CreateResponseMetaHeader(headerName, source string) Header {
jsonResponseMetaHeader, err := json.Marshal(ResponseMetaHeader{Source: source})
if err != nil {
panic(err)
}
return Header{
Name: headerName,
Value: string(jsonResponseMetaHeader),
}
}
+45 -23
View File
@@ -37,12 +37,19 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
"Mock header 2": {"Mock value 2"},
}
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
func createSerializedHeaders(headersField string, headers http.Header) []Header {
return []Header{{
headersField,
SerializeHeaders(headers),
}}
}
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
@@ -365,18 +372,18 @@ func randomASCIIPrintableChar(rand *rand.Rand) int {
// between 1 and `maxLength`.
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
length := minLength + rand.Intn(maxLength)
result := ""
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 += fmt.Sprintf("%%%02X", c)
result.WriteString(fmt.Sprintf("%%%02X", c))
} else {
result += string(c)
result.WriteByte(byte(c))
}
}
return result
return result.String()
}
// Calls `randomASCIIText` and ensures the result is a valid URL path,
@@ -509,40 +516,32 @@ func TestParseHeaders(t *testing.T) {
}
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "One", Value: "1"}, // will be dropped
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
}
expectedHeaders := []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"},
}
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, parsedHeaders)
}
func TestParseHeadersNoSerializedHeader(t *testing.T) {
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
h1 := &http.Request{
Header: make(http.Header),
}
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
err := H2RequestHeadersToH1Request(mockHeaders, h1)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
}
func TestIsControlHeader(t *testing.T) {
controlHeaders := []string{
// Anything that begins with cf-
"cf-sample-header",
"CF-SAMPLE-HEADER",
"Cf-Sample-Header",
// Any http2 pseudoheader
":sample-pseudo-header",
@@ -559,8 +558,8 @@ func TestIsControlHeader(t *testing.T) {
func TestIsNotControlHeader(t *testing.T) {
notControlHeaders := []string{
"Mock-header",
"Another-sample-header",
"mock-header",
"another-sample-header",
}
for _, header := range notControlHeaders {
@@ -650,3 +649,26 @@ func randSeq(n int) string {
}
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)
}
}
+31 -5
View File
@@ -138,6 +138,14 @@ func (s *MuxedStream) Write(p []byte) (int, error) {
if s.writeEOF {
return 0, io.EOF
}
// pre-allocate some space in the write buffer if possible
if buffer, ok := s.writeBuffer.(*bytes.Buffer); ok {
if buffer.Cap() == 0 {
buffer.Grow(writeBufferInitialSize)
}
}
totalWritten := 0
for totalWritten < len(p) {
// If the buffer is full, block till there is more room.
@@ -367,7 +375,9 @@ type streamChunk struct {
// true if data frames should be sent
sendData bool
eof bool
buffer bytes.Buffer
buffer []byte
offset int
}
// getChunk atomically extracts a chunk of data to be written by MuxWriter.
@@ -385,8 +395,17 @@ func (s *MuxedStream) getChunk() *streamChunk {
eof: s.writeEOF && uint32(s.writeBuffer.Len()) <= s.sendWindow,
}
// Copy at most s.sendWindow bytes, adjust the sendWindow accordingly
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
s.sendWindow -= uint32(writeLen)
toCopy := int(s.sendWindow)
if toCopy > s.writeBuffer.Len() {
toCopy = s.writeBuffer.Len()
}
if toCopy > 0 {
buf := make([]byte, toCopy)
writeLen, _ := s.writeBuffer.Read(buf)
chunk.buffer = buf[:writeLen]
s.sendWindow -= uint32(writeLen)
}
// Allow MuxedStream::Write() to continue, if needed
if s.writeBuffer.Len() < s.writeBufferMaxLen {
@@ -421,8 +440,15 @@ func (c *streamChunk) sendDataFrame() bool {
}
func (c *streamChunk) nextDataFrame(frameSize int) (payload []byte, endStream bool) {
payload = c.buffer.Next(frameSize)
if c.buffer.Len() == 0 {
bytesLeft := len(c.buffer) - c.offset
if frameSize > bytesLeft {
frameSize = bytesLeft
}
nextOffset := c.offset + frameSize
payload = c.buffer[c.offset:nextOffset]
c.offset = nextOffset
if c.offset == len(c.buffer) {
// this is the last data frame in this chunk
c.sendData = false
if c.eof {
-12
View File
@@ -6,7 +6,6 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/golang-collections/collections/queue"
"github.com/prometheus/client_golang/prometheus"
)
// data points used to compute average receive window and send window size
@@ -295,14 +294,3 @@ func (r *rate) get() (curr, min, max uint64) {
defer r.lock.RUnlock()
return r.curr, r.min, r.max
}
func NewActiveStreamsMetrics(namespace, subsystem string) prometheus.Gauge {
activeStreams := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "active_streams",
Help: "Number of active streams created by all muxers.",
})
prometheus.MustRegister(activeStreams)
return activeStreams
}
+61 -2
View File
@@ -18,6 +18,14 @@ import (
"github.com/cloudflare/cloudflared/tlsconfig"
)
const (
UptimeRoute = "/uptime"
WSRoute = "/ws"
SSERoute = "/sse"
HealthRoute = "/_health"
defaultSSEFreq = time.Second * 10
)
type templateData struct {
ServerName string
Request *http.Request
@@ -104,8 +112,10 @@ func StartHelloWorldServer(logger logger.Service, listener net.Listener, shutdow
}
muxer := http.NewServeMux()
muxer.HandleFunc("/uptime", uptimeHandler(time.Now()))
muxer.HandleFunc("/ws", websocketHandler(logger, upgrader))
muxer.HandleFunc(UptimeRoute, uptimeHandler(time.Now()))
muxer.HandleFunc(WSRoute, websocketHandler(logger, upgrader))
muxer.HandleFunc(SSERoute, sseHandler(logger))
muxer.HandleFunc(HealthRoute, healthHandler())
muxer.HandleFunc("/", rootHandler(serverName))
httpServer := &http.Server{Addr: listener.Addr().String(), Handler: muxer}
go func() {
@@ -150,8 +160,15 @@ func uptimeHandler(startTime time.Time) http.HandlerFunc {
// This handler will echo message
func websocketHandler(logger logger.Service, upgrader websocket.Upgrader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// This addresses the issue of r.Host includes port but origin header doesn't
host, _, err := net.SplitHostPort(r.Host)
if err == nil {
r.Host = host
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
logger.Errorf("failed to upgrade to websocket connection, error: %s", err)
return
}
defer conn.Close()
@@ -170,6 +187,48 @@ func websocketHandler(logger logger.Service, upgrader websocket.Upgrader) http.H
}
}
func sseHandler(logger logger.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
flusher, ok := w.(http.Flusher)
if !ok {
w.WriteHeader(http.StatusInternalServerError)
logger.Errorf("Can't support SSE. ResponseWriter %T doesn't implement http.Flusher interface", w)
return
}
freq := defaultSSEFreq
if requestedFreq := r.URL.Query()["freq"]; len(requestedFreq) > 0 {
parsedFreq, err := time.ParseDuration(requestedFreq[0])
if err == nil {
freq = parsedFreq
}
}
logger.Infof("Server Sent Events every %s", freq)
ticker := time.NewTicker(freq)
counter := 0
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
}
_, err := fmt.Fprintf(w, "%d\n\n", counter)
if err != nil {
return
}
flusher.Flush()
counter++
}
}
}
func healthHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}
}
func rootHandler(serverName string) http.HandlerFunc {
responseTemplate := template.Must(template.New("index").Parse(indexTemplate))
return func(w http.ResponseWriter, r *http.Request) {
+237
View File
@@ -0,0 +1,237 @@
package ingress
import (
"fmt"
"net"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
var (
ErrNoIngressRules = errors.New("The config file doesn't contain any ingress rules")
errLastRuleNotCatchAll = errors.New("The last ingress rule must match all URLs (i.e. it should not have a hostname or path filter)")
errBadWildcard = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"")
errHostnameContainsPort = errors.New("Hostname cannot contain a port")
ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules")
)
// FindMatchingRule returns the index of the Ingress Rule which matches the given
// hostname and path. This function assumes the last rule matches everything,
// which is the case if the rules were instantiated via the ingress#Validate method
func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
// The hostname might contain port. We only want to compare the host part with the rule
host, _, err := net.SplitHostPort(hostname)
if err == nil {
hostname = host
}
for i, rule := range ing.Rules {
if rule.Matches(hostname, path) {
return &rule, i
}
}
i := len(ing.Rules) - 1
return &ing.Rules[i], i
}
func matchHost(ruleHost, reqHost string) bool {
if ruleHost == reqHost {
return true
}
// Validate hostnames that use wildcards at the start
if strings.HasPrefix(ruleHost, "*.") {
toMatch := strings.TrimPrefix(ruleHost, "*.")
return strings.HasSuffix(reqHost, toMatch)
}
return false
}
// Ingress maps eyeball requests to origins.
type Ingress struct {
Rules []Rule
defaults OriginRequestConfig
}
// NewSingleOrigin constructs an Ingress set with only one rule, constructed from
// legacy CLI parameters like --url or --no-chunked-encoding.
func NewSingleOrigin(c *cli.Context, allowURLFromArgs bool, logger logger.Service) (Ingress, error) {
service, err := parseSingleOriginService(c, allowURLFromArgs)
if err != nil {
return Ingress{}, err
}
// Construct an Ingress with the single rule.
defaults := originRequestFromSingeRule(c)
ing := Ingress{
Rules: []Rule{
{
Service: service,
Config: setConfig(defaults, config.OriginRequestConfig{}),
},
},
defaults: defaults,
}
return ing, err
}
// Get a single origin service from the CLI/config.
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginService, error) {
if c.IsSet("hello-world") {
return new(helloWorld), nil
}
if c.IsSet("url") || c.IsSet(config.BastionFlag) {
originURL, err := config.ValidateUrl(c, allowURLFromArgs)
if err != nil {
return nil, errors.Wrap(err, "Error validating origin URL")
}
return &localService{URL: originURL, RootURL: originURL}, nil
}
if c.IsSet("unix-socket") {
path, err := config.ValidateUnixSocket(c)
if err != nil {
return nil, errors.Wrap(err, "Error validating --unix-socket")
}
return &unixSocketPath{path: path}, nil
}
u, err := url.Parse("http://localhost:8080")
return &localService{URL: u, RootURL: u}, err
}
// IsEmpty checks if there are any ingress rules.
func (ing Ingress) IsEmpty() bool {
return len(ing.Rules) == 0
}
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
func (ing Ingress) StartOrigins(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error) {
for _, rule := range ing.Rules {
if err := rule.Service.start(wg, log, shutdownC, errC, rule.Config); err != nil {
log.Errorf("Error starting local service %s: %s", rule.Service, err)
}
}
}
// CatchAll returns the catch-all rule (i.e. the last rule)
func (ing Ingress) CatchAll() *Rule {
return &ing.Rules[len(ing.Rules)-1]
}
func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) {
rules := make([]Rule, len(ingress))
for i, r := range ingress {
cfg := setConfig(defaults, r.OriginRequest)
var service OriginService
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
// No validation necessary for unix socket filepath services
path := strings.TrimPrefix(r.Service, prefix)
service = &unixSocketPath{path: path}
} else if prefix := "http_status:"; strings.HasPrefix(r.Service, prefix) {
status, err := strconv.Atoi(strings.TrimPrefix(r.Service, prefix))
if err != nil {
return Ingress{}, errors.Wrap(err, "invalid HTTP status")
}
srv := newStatusCode(status)
service = &srv
} else if r.Service == "hello_world" || r.Service == "hello-world" || r.Service == "helloworld" {
service = new(helloWorld)
} else if r.Service == "bastion" || cfg.BastionMode {
// Bastion mode will always start a Websocket proxy server, which will
// overwrite the localService.URL field when `start` is called. So,
// leave the URL field empty for now.
cfg.BastionMode = true
service = new(localService)
} else {
// Validate URL services
u, err := url.Parse(r.Service)
if err != nil {
return Ingress{}, err
}
if u.Scheme == "" || u.Hostname() == "" {
return Ingress{}, fmt.Errorf("%s is an invalid address, please make sure it has a scheme and a hostname", r.Service)
}
if u.Path != "" {
return Ingress{}, fmt.Errorf("%s is an invalid address, ingress rules don't support proxying to a different path on the origin service. The path will be the same as the eyeball request's path", r.Service)
}
serviceURL := localService{URL: u}
service = &serviceURL
}
if err := validateHostname(r, i, len(ingress)); err != nil {
return Ingress{}, err
}
var pathRegex *regexp.Regexp
if r.Path != "" {
var err error
pathRegex, err = regexp.Compile(r.Path)
if err != nil {
return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1)
}
}
rules[i] = Rule{
Hostname: r.Hostname,
Service: service,
Path: pathRegex,
Config: cfg,
}
}
return Ingress{Rules: rules, defaults: defaults}, nil
}
func validateHostname(r config.UnvalidatedIngressRule, ruleIndex, totalRules int) error {
// Ensure that the hostname doesn't contain port
_, _, err := net.SplitHostPort(r.Hostname)
if err == nil {
return errHostnameContainsPort
}
// Ensure that there are no wildcards anywhere except the first character
// of the hostname.
if strings.LastIndex(r.Hostname, "*") > 0 {
return errBadWildcard
}
// The last rule should catch all hostnames.
isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == ""
isLastRule := ruleIndex == totalRules-1
if isLastRule && !isCatchAllRule {
return errLastRuleNotCatchAll
}
// ONLY the last rule should catch all hostnames.
if !isLastRule && isCatchAllRule {
return errRuleShouldNotBeCatchAll{index: ruleIndex, hostname: r.Hostname}
}
return nil
}
type errRuleShouldNotBeCatchAll struct {
index int
hostname string
}
func (e errRuleShouldNotBeCatchAll) Error() string {
return fmt.Sprintf("Rule #%d is matching the hostname '%s', but "+
"this will match every hostname, meaning the rules which follow it "+
"will never be triggered.", e.index+1, e.hostname)
}
// ParseIngress parses ingress rules, but does not send HTTP requests to the origins.
func ParseIngress(conf *config.Configuration) (Ingress, error) {
if len(conf.Ingress) == 0 {
return Ingress{}, ErrNoIngressRules
}
return validate(conf.Ingress, originRequestFromYAML(conf.OriginRequest))
}
+455
View File
@@ -0,0 +1,455 @@
package ingress
import (
"flag"
"fmt"
"net/url"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tlsconfig"
)
func TestParseUnixSocket(t *testing.T) {
rawYAML := `
ingress:
- service: unix:/tmp/echo.sock
`
ing, err := ParseIngress(MustReadIngress(rawYAML))
require.NoError(t, err)
_, ok := ing.Rules[0].Service.(*unixSocketPath)
require.True(t, ok)
}
func Test_parseIngress(t *testing.T) {
localhost8000 := MustParseURL(t, "https://localhost:8000")
localhost8001 := MustParseURL(t, "https://localhost:8001")
fourOhFour := newStatusCode(404)
defaultConfig := setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{})
require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections)
tr := true
type args struct {
rawYAML string
}
tests := []struct {
name string
args args
want []Rule
wantErr bool
}{
{
name: "Empty file",
args: args{rawYAML: ""},
wantErr: true,
},
{
name: "Multiple rules",
args: args{rawYAML: `
ingress:
- hostname: tunnel1.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
`},
want: []Rule{
{
Hostname: "tunnel1.example.com",
Service: &localService{URL: localhost8000},
Config: defaultConfig,
},
{
Hostname: "*",
Service: &localService{URL: localhost8001},
Config: defaultConfig,
},
},
},
{
name: "Extra keys",
args: args{rawYAML: `
ingress:
- hostname: "*"
service: https://localhost:8000
extraKey: extraValue
`},
want: []Rule{
{
Hostname: "*",
Service: &localService{URL: localhost8000},
Config: defaultConfig,
},
},
},
{
name: "Hostname can be omitted",
args: args{rawYAML: `
ingress:
- service: https://localhost:8000
`},
want: []Rule{
{
Service: &localService{URL: localhost8000},
Config: defaultConfig,
},
},
},
{
name: "Invalid service",
args: args{rawYAML: `
ingress:
- hostname: "*"
service: https://local host:8000
`},
wantErr: true,
},
{
name: "Last rule isn't catchall",
args: args{rawYAML: `
ingress:
- hostname: example.com
service: https://localhost:8000
`},
wantErr: true,
},
{
name: "First rule is catchall",
args: args{rawYAML: `
ingress:
- service: https://localhost:8000
- hostname: example.com
service: https://localhost:8000
`},
wantErr: true,
},
{
name: "Catch-all rule can't have a path",
args: args{rawYAML: `
ingress:
- service: https://localhost:8001
path: /subpath1/(.*)/subpath2
`},
wantErr: true,
},
{
name: "Invalid regex",
args: args{rawYAML: `
ingress:
- hostname: example.com
service: https://localhost:8000
path: "*/subpath2"
- service: https://localhost:8001
`},
wantErr: true,
},
{
name: "Service must have a scheme",
args: args{rawYAML: `
ingress:
- service: localhost:8000
`},
wantErr: true,
},
{
name: "Wildcard not at start",
args: args{rawYAML: `
ingress:
- hostname: "test.*.example.com"
service: https://localhost:8000
`},
wantErr: true,
},
{
name: "Service can't have a path",
args: args{rawYAML: `
ingress:
- service: https://localhost:8000/static/
`},
wantErr: true,
},
{
name: "Invalid HTTP status",
args: args{rawYAML: `
ingress:
- service: http_status:asdf
`},
wantErr: true,
},
{
name: "Valid HTTP status",
args: args{rawYAML: `
ingress:
- service: http_status:404
`},
want: []Rule{
{
Hostname: "",
Service: &fourOhFour,
Config: defaultConfig,
},
},
},
{
name: "Valid hello world service",
args: args{rawYAML: `
ingress:
- service: hello_world
`},
want: []Rule{
{
Hostname: "",
Service: new(helloWorld),
Config: defaultConfig,
},
},
},
{
name: "URL isn't necessary if using bastion",
args: args{rawYAML: `
ingress:
- hostname: bastion.foo.com
originRequest:
bastionMode: true
- service: http_status:404
`},
want: []Rule{
{
Hostname: "bastion.foo.com",
Service: &localService{},
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
},
{
Service: &fourOhFour,
Config: defaultConfig,
},
},
},
{
name: "Bastion service",
args: args{rawYAML: `
ingress:
- hostname: bastion.foo.com
service: bastion
- service: http_status:404
`},
want: []Rule{
{
Hostname: "bastion.foo.com",
Service: &localService{},
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
},
{
Service: &fourOhFour,
Config: defaultConfig,
},
},
},
{
name: "Hostname contains port",
args: args{rawYAML: `
ingress:
- hostname: "test.example.com:443"
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseIngress(MustReadIngress(tt.args.rawYAML))
if (err != nil) != tt.wantErr {
t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr)
return
}
require.Equal(t, tt.want, got.Rules)
})
}
}
func TestSingleOriginSetsConfig(t *testing.T) {
flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError)
flagSet.Bool("hello-world", true, "")
flagSet.Duration(ProxyConnectTimeoutFlag, time.Second, "")
flagSet.Duration(ProxyTLSTimeoutFlag, time.Second, "")
flagSet.Duration(ProxyTCPKeepAlive, time.Second, "")
flagSet.Bool(ProxyNoHappyEyeballsFlag, true, "")
flagSet.Int(ProxyKeepAliveConnectionsFlag, 10, "")
flagSet.Duration(ProxyKeepAliveTimeoutFlag, time.Second, "")
flagSet.String(HTTPHostHeaderFlag, "example.com:8080", "")
flagSet.String(OriginServerNameFlag, "example.com", "")
flagSet.String(tlsconfig.OriginCAPoolFlag, "/etc/certs/ca.pem", "")
flagSet.Bool(NoTLSVerifyFlag, true, "")
flagSet.Bool(NoChunkedEncodingFlag, true, "")
flagSet.Bool(config.BastionFlag, true, "")
flagSet.String(ProxyAddressFlag, "localhost:8080", "")
flagSet.Uint(ProxyPortFlag, 8080, "")
flagSet.Bool(Socks5Flag, true, "")
cliCtx := cli.NewContext(cli.NewApp(), flagSet, nil)
err := cliCtx.Set("hello-world", "true")
require.NoError(t, err)
err = cliCtx.Set(ProxyConnectTimeoutFlag, "1s")
require.NoError(t, err)
err = cliCtx.Set(ProxyTLSTimeoutFlag, "1s")
require.NoError(t, err)
err = cliCtx.Set(ProxyTCPKeepAlive, "1s")
require.NoError(t, err)
err = cliCtx.Set(ProxyNoHappyEyeballsFlag, "true")
require.NoError(t, err)
err = cliCtx.Set(ProxyKeepAliveConnectionsFlag, "10")
require.NoError(t, err)
err = cliCtx.Set(ProxyKeepAliveTimeoutFlag, "1s")
require.NoError(t, err)
err = cliCtx.Set(HTTPHostHeaderFlag, "example.com:8080")
require.NoError(t, err)
err = cliCtx.Set(OriginServerNameFlag, "example.com")
require.NoError(t, err)
err = cliCtx.Set(tlsconfig.OriginCAPoolFlag, "/etc/certs/ca.pem")
require.NoError(t, err)
err = cliCtx.Set(NoTLSVerifyFlag, "true")
require.NoError(t, err)
err = cliCtx.Set(NoChunkedEncodingFlag, "true")
require.NoError(t, err)
err = cliCtx.Set(config.BastionFlag, "true")
require.NoError(t, err)
err = cliCtx.Set(ProxyAddressFlag, "localhost:8080")
require.NoError(t, err)
err = cliCtx.Set(ProxyPortFlag, "8080")
require.NoError(t, err)
err = cliCtx.Set(Socks5Flag, "true")
require.NoError(t, err)
allowURLFromArgs := false
logger, err := logger.New()
require.NoError(t, err)
ingress, err := NewSingleOrigin(cliCtx, allowURLFromArgs, logger)
require.NoError(t, err)
assert.Equal(t, time.Second, ingress.Rules[0].Config.ConnectTimeout)
assert.Equal(t, time.Second, ingress.Rules[0].Config.TLSTimeout)
assert.Equal(t, time.Second, ingress.Rules[0].Config.TCPKeepAlive)
assert.True(t, ingress.Rules[0].Config.NoHappyEyeballs)
assert.Equal(t, 10, ingress.Rules[0].Config.KeepAliveConnections)
assert.Equal(t, time.Second, ingress.Rules[0].Config.KeepAliveTimeout)
assert.Equal(t, "example.com:8080", ingress.Rules[0].Config.HTTPHostHeader)
assert.Equal(t, "example.com", ingress.Rules[0].Config.OriginServerName)
assert.Equal(t, "/etc/certs/ca.pem", ingress.Rules[0].Config.CAPool)
assert.True(t, ingress.Rules[0].Config.NoTLSVerify)
assert.True(t, ingress.Rules[0].Config.DisableChunkedEncoding)
assert.True(t, ingress.Rules[0].Config.BastionMode)
assert.Equal(t, "localhost:8080", ingress.Rules[0].Config.ProxyAddress)
assert.Equal(t, uint(8080), ingress.Rules[0].Config.ProxyPort)
assert.Equal(t, socksProxy, ingress.Rules[0].Config.ProxyType)
}
func TestFindMatchingRule(t *testing.T) {
ingress := Ingress{
Rules: []Rule{
{
Hostname: "tunnel-a.example.com",
Path: nil,
},
{
Hostname: "tunnel-b.example.com",
Path: mustParsePath(t, "/health"),
},
{
Hostname: "*",
},
},
}
tests := []struct {
host string
path string
wantRuleIndex int
}{
{
host: "tunnel-a.example.com",
path: "/",
wantRuleIndex: 0,
},
{
host: "tunnel-a.example.com",
path: "/pages/about",
wantRuleIndex: 0,
},
{
host: "tunnel-a.example.com:443",
path: "/pages/about",
wantRuleIndex: 0,
},
{
host: "tunnel-b.example.com",
path: "/health",
wantRuleIndex: 1,
},
{
host: "tunnel-b.example.com",
path: "/index.html",
wantRuleIndex: 2,
},
{
host: "tunnel-c.example.com",
path: "/",
wantRuleIndex: 2,
},
}
for i, test := range tests {
_, ruleIndex := ingress.FindMatchingRule(test.host, test.path)
assert.Equal(t, test.wantRuleIndex, ruleIndex, fmt.Sprintf("Expect host=%s, path=%s to match rule %d, got %d", test.host, test.path, test.wantRuleIndex, i))
}
}
func mustParsePath(t *testing.T, path string) *regexp.Regexp {
regexp, err := regexp.Compile(path)
assert.NoError(t, err)
return regexp
}
func MustParseURL(t *testing.T, rawURL string) *url.URL {
u, err := url.Parse(rawURL)
require.NoError(t, err)
return u
}
func BenchmarkFindMatch(b *testing.B) {
rulesYAML := `
ingress:
- hostname: tunnel1.example.com
service: https://localhost:8000
- hostname: tunnel2.example.com
service: https://localhost:8001
- hostname: "*"
service: https://localhost:8002
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
b.Error(err)
}
for n := 0; n < b.N; n++ {
ing.FindMatchingRule("tunnel1.example.com", "")
ing.FindMatchingRule("tunnel2.example.com", "")
ing.FindMatchingRule("tunnel3.example.com", "")
}
}
func MustReadIngress(s string) *config.Configuration {
var conf config.Configuration
err := yaml.Unmarshal([]byte(s), &conf)
if err != nil {
panic(err)
}
return &conf
}
+331
View File
@@ -0,0 +1,331 @@
package ingress
import (
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/urfave/cli/v2"
)
const (
defaultConnectTimeout = 30 * time.Second
defaultTLSTimeout = 10 * time.Second
defaultTCPKeepAlive = 30 * time.Second
defaultKeepAliveConnections = 100
defaultKeepAliveTimeout = 90 * time.Second
defaultProxyAddress = "127.0.0.1"
SSHServerFlag = "ssh-server"
Socks5Flag = "socks5"
ProxyConnectTimeoutFlag = "proxy-connect-timeout"
ProxyTLSTimeoutFlag = "proxy-tls-timeout"
ProxyTCPKeepAlive = "proxy-tcp-keepalive"
ProxyNoHappyEyeballsFlag = "proxy-no-happy-eyeballs"
ProxyKeepAliveConnectionsFlag = "proxy-keepalive-connections"
ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout"
HTTPHostHeaderFlag = "http-host-header"
OriginServerNameFlag = "origin-server-name"
NoTLSVerifyFlag = "no-tls-verify"
NoChunkedEncodingFlag = "no-chunked-encoding"
ProxyAddressFlag = "proxy-address"
ProxyPortFlag = "proxy-port"
)
const (
socksProxy = "socks"
)
func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
var connectTimeout time.Duration = defaultConnectTimeout
var tlsTimeout time.Duration = defaultTLSTimeout
var tcpKeepAlive time.Duration = defaultTCPKeepAlive
var noHappyEyeballs bool
var keepAliveConnections int = defaultKeepAliveConnections
var keepAliveTimeout time.Duration = defaultKeepAliveTimeout
var httpHostHeader string
var originServerName string
var caPool string
var noTLSVerify bool
var disableChunkedEncoding bool
var bastionMode bool
var proxyAddress = defaultProxyAddress
var proxyPort uint
var proxyType string
if flag := ProxyConnectTimeoutFlag; c.IsSet(flag) {
connectTimeout = c.Duration(flag)
}
if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) {
tlsTimeout = c.Duration(flag)
}
if flag := ProxyTCPKeepAlive; c.IsSet(flag) {
tcpKeepAlive = c.Duration(flag)
}
if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) {
noHappyEyeballs = c.Bool(flag)
}
if flag := ProxyKeepAliveConnectionsFlag; c.IsSet(flag) {
keepAliveConnections = c.Int(flag)
}
if flag := ProxyKeepAliveTimeoutFlag; c.IsSet(flag) {
keepAliveTimeout = c.Duration(flag)
}
if flag := HTTPHostHeaderFlag; c.IsSet(flag) {
httpHostHeader = c.String(flag)
}
if flag := OriginServerNameFlag; c.IsSet(flag) {
originServerName = c.String(flag)
}
if flag := tlsconfig.OriginCAPoolFlag; c.IsSet(flag) {
caPool = c.String(flag)
}
if flag := NoTLSVerifyFlag; c.IsSet(flag) {
noTLSVerify = c.Bool(flag)
}
if flag := NoChunkedEncodingFlag; c.IsSet(flag) {
disableChunkedEncoding = c.Bool(flag)
}
if flag := config.BastionFlag; c.IsSet(flag) {
bastionMode = c.Bool(flag)
}
if flag := ProxyAddressFlag; c.IsSet(flag) {
proxyAddress = c.String(flag)
}
if flag := ProxyPortFlag; c.IsSet(flag) {
proxyPort = c.Uint(flag)
}
if c.IsSet(Socks5Flag) {
proxyType = socksProxy
}
return OriginRequestConfig{
ConnectTimeout: connectTimeout,
TLSTimeout: tlsTimeout,
TCPKeepAlive: tcpKeepAlive,
NoHappyEyeballs: noHappyEyeballs,
KeepAliveConnections: keepAliveConnections,
KeepAliveTimeout: keepAliveTimeout,
HTTPHostHeader: httpHostHeader,
OriginServerName: originServerName,
CAPool: caPool,
NoTLSVerify: noTLSVerify,
DisableChunkedEncoding: disableChunkedEncoding,
BastionMode: bastionMode,
ProxyAddress: proxyAddress,
ProxyPort: proxyPort,
ProxyType: proxyType,
}
}
func originRequestFromYAML(y config.OriginRequestConfig) OriginRequestConfig {
out := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
if y.ConnectTimeout != nil {
out.ConnectTimeout = *y.ConnectTimeout
}
if y.TLSTimeout != nil {
out.TLSTimeout = *y.TLSTimeout
}
if y.TCPKeepAlive != nil {
out.TCPKeepAlive = *y.TCPKeepAlive
}
if y.NoHappyEyeballs != nil {
out.NoHappyEyeballs = *y.NoHappyEyeballs
}
if y.KeepAliveConnections != nil {
out.KeepAliveConnections = *y.KeepAliveConnections
}
if y.KeepAliveTimeout != nil {
out.KeepAliveTimeout = *y.KeepAliveTimeout
}
if y.HTTPHostHeader != nil {
out.HTTPHostHeader = *y.HTTPHostHeader
}
if y.OriginServerName != nil {
out.OriginServerName = *y.OriginServerName
}
if y.CAPool != nil {
out.CAPool = *y.CAPool
}
if y.NoTLSVerify != nil {
out.NoTLSVerify = *y.NoTLSVerify
}
if y.DisableChunkedEncoding != nil {
out.DisableChunkedEncoding = *y.DisableChunkedEncoding
}
if y.BastionMode != nil {
out.BastionMode = *y.BastionMode
}
if y.ProxyAddress != nil {
out.ProxyAddress = *y.ProxyAddress
}
if y.ProxyPort != nil {
out.ProxyPort = *y.ProxyPort
}
if y.ProxyType != nil {
out.ProxyType = *y.ProxyType
}
return out
}
// OriginRequestConfig configures how Cloudflared sends requests to origin
// services.
// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
type OriginRequestConfig struct {
// HTTP proxy timeout for establishing a new connection
ConnectTimeout time.Duration `yaml:"connectTimeout"`
// HTTP proxy timeout for completing a TLS handshake
TLSTimeout time.Duration `yaml:"tlsTimeout"`
// HTTP proxy TCP keepalive duration
TCPKeepAlive time.Duration `yaml:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs bool `yaml:"noHappyEyeballs"`
// HTTP proxy maximum keepalive connection pool size
KeepAliveConnections int `yaml:"keepAliveConnections"`
// HTTP proxy timeout for closing an idle connection
KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"`
// Sets the HTTP Host header for the local webserver.
HTTPHostHeader string `yaml:"httpHostHeader"`
// Hostname on the origin server certificate.
OriginServerName string `yaml:"originServerName"`
// 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"`
// Disables TLS verification of the certificate presented by your origin.
// Will allow any certificate from the origin to be accepted.
// Note: The connection from your machine to Cloudflare's Edge is still encrypted.
NoTLSVerify bool `yaml:"noTLSVerify"`
// Disables chunked transfer encoding.
// Useful if you are running a WSGI server.
DisableChunkedEncoding bool `yaml:"disableChunkedEncoding"`
// Runs as jump host
BastionMode bool `yaml:"bastionMode"`
// Listen address for the proxy.
ProxyAddress string `yaml:"proxyAddress"`
// Listen port for the proxy.
ProxyPort uint `yaml:"proxyPort"`
// What sort of proxy should be started
ProxyType string `yaml:"proxyType"`
}
func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) {
if val := overrides.ConnectTimeout; val != nil {
defaults.ConnectTimeout = *val
}
}
func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) {
if val := overrides.TLSTimeout; val != nil {
defaults.TLSTimeout = *val
}
}
func (defaults *OriginRequestConfig) setNoHappyEyeballs(overrides config.OriginRequestConfig) {
if val := overrides.NoHappyEyeballs; val != nil {
defaults.NoHappyEyeballs = *val
}
}
func (defaults *OriginRequestConfig) setKeepAliveConnections(overrides config.OriginRequestConfig) {
if val := overrides.KeepAliveConnections; val != nil {
defaults.KeepAliveConnections = *val
}
}
func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) {
if val := overrides.KeepAliveTimeout; val != nil {
defaults.KeepAliveTimeout = *val
}
}
func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) {
if val := overrides.TCPKeepAlive; val != nil {
defaults.TCPKeepAlive = *val
}
}
func (defaults *OriginRequestConfig) setHTTPHostHeader(overrides config.OriginRequestConfig) {
if val := overrides.HTTPHostHeader; val != nil {
defaults.HTTPHostHeader = *val
}
}
func (defaults *OriginRequestConfig) setOriginServerName(overrides config.OriginRequestConfig) {
if val := overrides.OriginServerName; val != nil {
defaults.OriginServerName = *val
}
}
func (defaults *OriginRequestConfig) setCAPool(overrides config.OriginRequestConfig) {
if val := overrides.CAPool; val != nil {
defaults.CAPool = *val
}
}
func (defaults *OriginRequestConfig) setNoTLSVerify(overrides config.OriginRequestConfig) {
if val := overrides.NoTLSVerify; val != nil {
defaults.NoTLSVerify = *val
}
}
func (defaults *OriginRequestConfig) setDisableChunkedEncoding(overrides config.OriginRequestConfig) {
if val := overrides.DisableChunkedEncoding; val != nil {
defaults.DisableChunkedEncoding = *val
}
}
func (defaults *OriginRequestConfig) setBastionMode(overrides config.OriginRequestConfig) {
if val := overrides.BastionMode; val != nil {
defaults.BastionMode = *val
}
}
func (defaults *OriginRequestConfig) setProxyPort(overrides config.OriginRequestConfig) {
if val := overrides.ProxyPort; val != nil {
defaults.ProxyPort = *val
}
}
func (defaults *OriginRequestConfig) setProxyAddress(overrides config.OriginRequestConfig) {
if val := overrides.ProxyAddress; val != nil {
defaults.ProxyAddress = *val
}
}
func (defaults *OriginRequestConfig) setProxyType(overrides config.OriginRequestConfig) {
if val := overrides.ProxyType; val != nil {
defaults.ProxyType = *val
}
}
// SetConfig gets config for the requests that cloudflared sends to origins.
// Each field has a setter method which sets a value for the field by trying to find:
// 1. The user config for this rule
// 2. The user config for the overall ingress config
// 3. Defaults chosen by the cloudflared team
// 4. Golang zero values for that type
// If an earlier option isn't set, it will try the next option down.
func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfig) OriginRequestConfig {
cfg := defaults
cfg.setConnectTimeout(overrides)
cfg.setTLSTimeout(overrides)
cfg.setNoHappyEyeballs(overrides)
cfg.setKeepAliveConnections(overrides)
cfg.setKeepAliveTimeout(overrides)
cfg.setTCPKeepAlive(overrides)
cfg.setHTTPHostHeader(overrides)
cfg.setOriginServerName(overrides)
cfg.setCAPool(overrides)
cfg.setNoTLSVerify(overrides)
cfg.setDisableChunkedEncoding(overrides)
cfg.setBastionMode(overrides)
cfg.setProxyPort(overrides)
cfg.setProxyAddress(overrides)
cfg.setProxyType(overrides)
return cfg
}
+202
View File
@@ -0,0 +1,202 @@
package ingress
import (
"flag"
"testing"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
)
// Ensure that the nullable config from `config` package and the
// non-nullable config from `ingress` package have the same number of
// fields.
// This test ensures that programmers didn't add a new field to
// one struct and forget to add it to the other ;)
func TestCorrespondingFields(t *testing.T) {
require.Equal(
t,
CountFields(t, config.OriginRequestConfig{}),
CountFields(t, OriginRequestConfig{}),
)
}
func CountFields(t *testing.T, val interface{}) int {
b, err := yaml.Marshal(val)
require.NoError(t, err)
m := make(map[string]interface{}, 0)
err = yaml.Unmarshal(b, &m)
require.NoError(t, err)
return len(m)
}
func TestOriginRequestConfigOverrides(t *testing.T) {
rulesYAML := `
originRequest:
connectTimeout: 1m
tlsTimeout: 1s
noHappyEyeballs: true
tcpKeepAlive: 1s
keepAliveConnections: 1
keepAliveTimeout: 1s
httpHostHeader: abc
originServerName: a1
caPool: /tmp/path0
noTLSVerify: true
disableChunkedEncoding: true
bastionMode: True
proxyAddress: 127.1.2.3
proxyPort: 100
proxyType: socks5
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
t.Error(err)
}
// Rule 0 didn't override anything, so it inherits the user-specified
// root-level configuration.
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: 1 * time.Minute,
TLSTimeout: 1 * time.Second,
NoHappyEyeballs: true,
TCPKeepAlive: 1 * time.Second,
KeepAliveConnections: 1,
KeepAliveTimeout: 1 * time.Second,
HTTPHostHeader: "abc",
OriginServerName: "a1",
CAPool: "/tmp/path0",
NoTLSVerify: true,
DisableChunkedEncoding: true,
BastionMode: true,
ProxyAddress: "127.1.2.3",
ProxyPort: uint(100),
ProxyType: "socks5",
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all the root-level config.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
NoHappyEyeballs: false,
TCPKeepAlive: 2 * time.Second,
KeepAliveConnections: 2,
KeepAliveTimeout: 2 * time.Second,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
}
require.Equal(t, expected1, actual1)
}
func TestOriginRequestConfigDefaults(t *testing.T) {
rulesYAML := `
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
t.Error(err)
}
// Rule 0 didn't override anything, so it inherits the cloudflared defaults
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all defaults.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
NoHappyEyeballs: false,
TCPKeepAlive: 2 * time.Second,
KeepAliveConnections: 2,
KeepAliveTimeout: 2 * time.Second,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
}
require.Equal(t, expected1, actual1)
}
func TestDefaultConfigFromCLI(t *testing.T) {
set := flag.NewFlagSet("contrive", 0)
c := cli.NewContext(nil, set, nil)
expected := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
actual := originRequestFromSingeRule(c)
require.Equal(t, expected, actual)
}
+336
View File
@@ -0,0 +1,336 @@
package ingress
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/socks"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/websocket"
gws "github.com/gorilla/websocket"
"github.com/pkg/errors"
)
// OriginService is something a tunnel can proxy traffic to.
type OriginService interface {
// RoundTrip is how cloudflared proxies eyeball requests to the actual origin services
http.RoundTripper
String() string
// Start the origin service if it's managed by cloudflared, e.g. proxy servers or Hello World.
// If it's not managed by cloudflared, this is a no-op because the user is responsible for
// starting the origin service.
start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error
}
// unixSocketPath is an OriginService representing a unix socket (which accepts HTTP)
type unixSocketPath struct {
path string
transport *http.Transport
}
func (o *unixSocketPath) String() string {
return "unix socket: " + o.path
}
func (o *unixSocketPath) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg, log)
if err != nil {
return err
}
o.transport = transport
return nil
}
func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
return o.transport.RoundTrip(req)
}
func (o *unixSocketPath) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
d := &gws.Dialer{
NetDial: o.transport.Dial,
NetDialContext: o.transport.DialContext,
TLSClientConfig: o.transport.TLSClientConfig,
}
reqURL.Scheme = websocket.ChangeRequestScheme(reqURL)
return d.Dial(reqURL.String(), headers)
}
// localService is an OriginService listening on a TCP/IP address the user's origin can route to.
type localService struct {
// The URL for the user's origin service
RootURL *url.URL
// The URL that cloudflared should send requests to.
// If this origin requires starting a proxy, this is the proxy's address,
// and that proxy points to RootURL. Otherwise, this is equal to RootURL.
URL *url.URL
transport *http.Transport
}
func (o *localService) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
d := &gws.Dialer{TLSClientConfig: o.transport.TLSClientConfig}
// Rewrite the request URL so that it goes to the origin service.
reqURL.Host = o.URL.Host
reqURL.Scheme = websocket.ChangeRequestScheme(o.URL)
return d.Dial(reqURL.String(), headers)
}
func (o *localService) address() string {
return o.URL.String()
}
func (o *localService) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg, log)
if err != nil {
return err
}
o.transport = transport
// Start a proxy if one is needed
if staticHost := o.staticHost(); originRequiresProxy(staticHost, cfg) {
if err := o.startProxy(staticHost, wg, log, shutdownC, errC, cfg); err != nil {
return err
}
}
return nil
}
func (o *localService) startProxy(staticHost string, wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
// Start a listener for the proxy
proxyAddress := net.JoinHostPort(cfg.ProxyAddress, strconv.Itoa(int(cfg.ProxyPort)))
listener, err := net.Listen("tcp", proxyAddress)
if err != nil {
log.Errorf("Cannot start Websocket Proxy Server: %s", err)
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
}
// Start the proxy itself
wg.Add(1)
go func() {
defer wg.Done()
streamHandler := websocket.DefaultStreamHandler
// This origin's config specifies what type of proxy to start.
switch cfg.ProxyType {
case socksProxy:
log.Info("SOCKS5 server started")
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, _ http.Header) {
dialer := socks.NewConnDialer(remoteConn)
requestHandler := socks.NewRequestHandler(dialer)
socksServer := socks.NewConnectionHandler(requestHandler)
socksServer.Serve(wsConn)
}
case "":
log.Debug("Not starting any websocket proxy")
default:
log.Errorf("%s isn't a valid proxy (valid options are {%s})", cfg.ProxyType, socksProxy)
}
errC <- websocket.StartProxyServer(log, listener, staticHost, shutdownC, streamHandler)
}()
// Modify this origin, so that it no longer points at the origin service directly.
// Instead, it points at the proxy to the origin service.
newURL, err := url.Parse("http://" + listener.Addr().String())
if err != nil {
return err
}
o.URL = newURL
return nil
}
func (o *localService) String() string {
return o.address()
}
func (o *localService) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite the request URL so that it goes to the origin service.
req.URL.Host = o.URL.Host
req.URL.Scheme = o.URL.Scheme
return o.transport.RoundTrip(req)
}
func (o *localService) staticHost() string {
addPortIfMissing := func(uri *url.URL, port int) string {
if uri.Port() != "" {
return uri.Host
}
return fmt.Sprintf("%s:%d", uri.Hostname(), port)
}
switch o.URL.Scheme {
case "ssh":
return addPortIfMissing(o.URL, 22)
case "rdp":
return addPortIfMissing(o.URL, 3389)
case "smb":
return addPortIfMissing(o.URL, 445)
case "tcp":
return addPortIfMissing(o.URL, 7864) // just a random port since there isn't a default in this case
}
return ""
}
// HelloWorld is an OriginService for the built-in Hello World server.
// Users only use this for testing and experimenting with cloudflared.
type helloWorld struct {
server net.Listener
transport *http.Transport
}
func (o *helloWorld) String() string {
return "Hello World test origin"
}
// Start starts a HelloWorld server and stores its address in the Service receiver.
func (o *helloWorld) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg, log)
if err != nil {
return err
}
o.transport = transport
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
return errors.Wrap(err, "Cannot start Hello World Server")
}
wg.Add(1)
go func() {
defer wg.Done()
_ = hello.StartHelloWorldServer(log, helloListener, shutdownC)
}()
o.server = helloListener
return nil
}
func (o *helloWorld) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite the request URL so that it goes to the Hello World server.
req.URL.Host = o.server.Addr().String()
req.URL.Scheme = "https"
return o.transport.RoundTrip(req)
}
func (o *helloWorld) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
d := &gws.Dialer{
TLSClientConfig: o.transport.TLSClientConfig,
}
reqURL.Host = o.server.Addr().String()
reqURL.Scheme = "wss"
return d.Dial(reqURL.String(), headers)
}
func originRequiresProxy(staticHost string, cfg OriginRequestConfig) bool {
return staticHost != "" || cfg.BastionMode
}
// statusCode is an OriginService that just responds with a given HTTP status.
// Typical use-case is "user wants the catch-all rule to just respond 404".
type statusCode struct {
resp *http.Response
}
func newStatusCode(status int) statusCode {
resp := &http.Response{
StatusCode: status,
Status: fmt.Sprintf("%d %s", status, http.StatusText(status)),
Body: new(NopReadCloser),
}
return statusCode{resp: resp}
}
func (o *statusCode) String() string {
return fmt.Sprintf("HTTP %d", o.resp.StatusCode)
}
func (o *statusCode) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
return nil
}
func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) {
return o.resp, nil
}
type NopReadCloser struct{}
// Read always returns EOF to signal end of input
func (nrc *NopReadCloser) Read(buf []byte) (int, error) {
return 0, io.EOF
}
func (nrc *NopReadCloser) Close() error {
return nil
}
func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log logger.Service) (*http.Transport, error) {
originCertPool, err := tlsconfig.LoadOriginCA(cfg.CAPool, log)
if err != nil {
return nil, errors.Wrap(err, "Error loading cert pool")
}
httpTransport := http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: cfg.KeepAliveConnections,
MaxIdleConnsPerHost: cfg.KeepAliveConnections,
IdleConnTimeout: cfg.KeepAliveTimeout,
TLSHandshakeTimeout: cfg.TLSTimeout,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify},
}
if _, isHelloWorld := service.(*helloWorld); !isHelloWorld && cfg.OriginServerName != "" {
httpTransport.TLSClientConfig.ServerName = cfg.OriginServerName
}
dialer := &net.Dialer{
Timeout: cfg.ConnectTimeout,
KeepAlive: cfg.TCPKeepAlive,
}
if cfg.NoHappyEyeballs {
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
}
// DialContext depends on which kind of origin is being used.
dialContext := dialer.DialContext
switch service := service.(type) {
// If this origin is a unix socket, enforce network type "unix".
case *unixSocketPath:
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialContext(ctx, "unix", service.path)
}
// Otherwise, use the regular network config.
default:
httpTransport.DialContext = dialContext
}
return &httpTransport, nil
}
// MockOriginService should only be used by other packages to mock OriginService. Set Transport to configure desired RoundTripper behavior.
type MockOriginService struct {
Transport http.RoundTripper
}
func (mos MockOriginService) RoundTrip(req *http.Request) (*http.Response, error) {
return mos.Transport.RoundTrip(req)
}
func (mos MockOriginService) String() string {
return "MockOriginService"
}
func (mos MockOriginService) start(wg *sync.WaitGroup, log logger.Service, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
return nil
}
+50
View File
@@ -0,0 +1,50 @@
package ingress
import (
"regexp"
"strings"
)
// Rule routes traffic from a hostname/path on the public internet to the
// service running on the given URL.
type Rule struct {
// Requests for this hostname will be proxied to this rule's service.
Hostname string
// Path is an optional regex that can specify path-driven ingress rules.
Path *regexp.Regexp
// A (probably local) address. Requests for a hostname which matches this
// rule's hostname pattern will be proxied to the service running on this
// address.
Service OriginService
// Configure the request cloudflared sends to this specific origin.
Config OriginRequestConfig
}
// MultiLineString is for outputting rules in a human-friendly way when Cloudflared
// is used as a CLI tool (not as a daemon).
func (r Rule) MultiLineString() string {
var out strings.Builder
if r.Hostname != "" {
out.WriteString("\thostname: ")
out.WriteString(r.Hostname)
out.WriteRune('\n')
}
if r.Path != nil {
out.WriteString("\tpath: ")
out.WriteString(r.Path.String())
out.WriteRune('\n')
}
out.WriteString("\tservice: ")
out.WriteString(r.Service.String())
return out.String()
}
// Matches checks if the rule matches a given hostname/path combination.
func (r *Rule) Matches(hostname, path string) bool {
hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname)
pathMatch := r.Path == nil || r.Path.MatchString(path)
return hostMatch && pathMatch
}
+146
View File
@@ -0,0 +1,146 @@
package ingress
import (
"io"
"net/http/httptest"
"net/url"
"regexp"
"testing"
"github.com/stretchr/testify/require"
)
func Test_rule_matches(t *testing.T) {
type fields struct {
Hostname string
Path *regexp.Regexp
Service OriginService
}
type args struct {
requestURL *url.URL
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "Just hostname, pass",
fields: fields{
Hostname: "example.com",
},
args: args{
requestURL: MustParseURL(t, "https://example.com"),
},
want: true,
},
{
name: "Entire hostname is wildcard, should match everything",
fields: fields{
Hostname: "*",
},
args: args{
requestURL: MustParseURL(t, "https://example.com"),
},
want: true,
},
{
name: "Just hostname, fail",
fields: fields{
Hostname: "example.com",
},
args: args{
requestURL: MustParseURL(t, "https://foo.bar"),
},
want: false,
},
{
name: "Just wildcard hostname, pass",
fields: fields{
Hostname: "*.example.com",
},
args: args{
requestURL: MustParseURL(t, "https://adam.example.com"),
},
want: true,
},
{
name: "Just wildcard hostname, fail",
fields: fields{
Hostname: "*.example.com",
},
args: args{
requestURL: MustParseURL(t, "https://tunnel.com"),
},
want: false,
},
{
name: "Just wildcard outside of subdomain in hostname, fail",
fields: fields{
Hostname: "*example.com",
},
args: args{
requestURL: MustParseURL(t, "https://www.example.com"),
},
want: false,
},
{
name: "Wildcard over multiple subdomains",
fields: fields{
Hostname: "*.example.com",
},
args: args{
requestURL: MustParseURL(t, "https://adam.chalmers.example.com"),
},
want: true,
},
{
name: "Hostname and path",
fields: fields{
Hostname: "*.example.com",
Path: regexp.MustCompile("/static/.*\\.html"),
},
args: args{
requestURL: MustParseURL(t, "https://www.example.com/static/index.html"),
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := Rule{
Hostname: tt.fields.Hostname,
Path: tt.fields.Path,
Service: tt.fields.Service,
}
u := tt.args.requestURL
if got := r.Matches(u.Hostname(), u.Path); got != tt.want {
t.Errorf("rule.matches() = %v, want %v", got, tt.want)
}
})
}
}
func TestStaticHTTPStatus(t *testing.T) {
o := newStatusCode(404)
buf := make([]byte, 100)
sendReq := func() {
resp, err := o.RoundTrip(nil)
require.NoError(t, err)
_, err = resp.Body.Read(buf)
require.Equal(t, io.EOF, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 404, resp.StatusCode)
resp, err = o.RoundTrip(nil)
require.NoError(t, err)
w := httptest.NewRecorder()
n, err := io.Copy(w, resp.Body)
require.NoError(t, err)
require.Equal(t, int64(0), n)
}
sendReq()
sendReq()
}
+9
View File
@@ -0,0 +1,9 @@
---
basename: "cloudflared"
comment: "Cloudflare Argo Tunnel"
copyright: "Cloudflare, Inc"
arch: "x86"
abi: "64"
files:
- source: cloudflared
destination: /var/db/scripts/jet/cloudflared
+22 -10
View File
@@ -3,6 +3,7 @@ package logger
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
@@ -97,31 +98,32 @@ func Parse(opts ...Option) (*Options, error) {
// New setups a new logger based on the options.
// The default behavior is to write to standard out
func New(opts ...Option) (Service, error) {
config, err := Parse(opts...)
func New(opts ...Option) (*OutputWriter, error) {
options, err := Parse(opts...)
if err != nil {
return nil, err
}
l := NewOutputWriter(SharedWriteManager)
if config.logFileDirectory != "" {
l.Add(NewFileRollingWriter(config.logFileDirectory,
if options.logFileDirectory != "" {
l.Add(NewFileRollingWriter(SanitizeLogPath(options.logFileDirectory),
"cloudflared",
int64(config.maxFileSize),
config.maxFileCount),
NewDefaultFormatter(time.RFC3339Nano), config.supportedFileLevels...)
int64(options.maxFileSize),
options.maxFileCount),
NewDefaultFormatter(time.RFC3339Nano), options.supportedFileLevels...)
}
if !config.terminalOutputDisabled {
if !options.terminalOutputDisabled {
terminalFormatter := NewTerminalFormatter(time.RFC3339)
if len(config.supportedTerminalLevels) == 0 {
if len(options.supportedTerminalLevels) == 0 {
l.Add(os.Stderr, terminalFormatter, InfoLevel, ErrorLevel, FatalLevel)
} else {
l.Add(os.Stderr, terminalFormatter, config.supportedTerminalLevels...)
l.Add(os.Stderr, terminalFormatter, options.supportedTerminalLevels...)
}
}
return l, nil
}
@@ -139,3 +141,13 @@ func ParseLevelString(lvl string) ([]Level, error) {
}
return []Level{}, fmt.Errorf("not a valid log level: %q", lvl)
}
// SanitizeLogPath checks that the logger log path
func SanitizeLogPath(path string) string {
newPath := strings.TrimSpace(path)
// make sure it has a log file extension and is not a directory
if filepath.Ext(newPath) != ".log" && !(isDirectory(newPath) || strings.HasSuffix(newPath, "/")) {
newPath = newPath + ".log"
}
return newPath
}
+46
View File
@@ -0,0 +1,46 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogLevelParse(t *testing.T) {
lvls, err := ParseLevelString("fatal")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel}, lvls)
lvls, err = ParseLevelString("error")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel}, lvls)
lvls, err = ParseLevelString("info")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("info")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("warn")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("debug")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel, DebugLevel}, lvls)
_, err = ParseLevelString("blah")
assert.Error(t, err)
_, err = ParseLevelString("")
assert.Error(t, err)
}
func TestPathSanitizer(t *testing.T) {
assert.Equal(t, "somebad/path/log.bat.log", SanitizeLogPath("\t somebad/path/log.bat\n\n"))
assert.Equal(t, "proper/path/cloudflared.log", SanitizeLogPath("proper/path/cloudflared.log"))
assert.Equal(t, "proper/path/", SanitizeLogPath("proper/path/"))
assert.Equal(t, "proper/path/cloudflared.log", SanitizeLogPath("\tproper/path/cloudflared\n\n"))
}
+42 -7
View File
@@ -8,17 +8,17 @@ import (
"github.com/acmacalister/skittles"
)
// Level of logging
// Level of logging, lower number means more verbose logging, higher more terse
type Level int
const (
// InfoLevel is for standard log messages
InfoLevel Level = iota
// DebugLevel is for messages that are intended for purposes debugging only
DebugLevel
DebugLevel Level = iota
// ErrorLevel is for error message to indicte something has gone wrong
// InfoLevel is for standard log messages
InfoLevel
// ErrorLevel is for error message to indicate something has gone wrong
ErrorLevel
// FatalLevel is for error message that log and kill the program with an os.exit(1)
@@ -63,6 +63,12 @@ type TerminalFormatter struct {
supportsColor bool
}
// UIFormatter is used for streaming logs to UI
type UIFormatter struct {
format string
supportsColor bool
}
// NewTerminalFormatter creates a Terminal formatter for colored output
// format is the time format to use for timestamp formatting
func NewTerminalFormatter(format string) Formatter {
@@ -73,10 +79,39 @@ func NewTerminalFormatter(format string) Formatter {
}
}
func NewUIFormatter(format string) Formatter {
supportsColor := (runtime.GOOS != "windows")
return &UIFormatter{
format: format,
supportsColor: supportsColor,
}
}
// Timestamp uses formatting that is tview-specific for UI
func (f *UIFormatter) Timestamp(l Level, d time.Time) string {
t := ""
dateStr := "[" + d.Format(f.format) + "] "
switch l {
case InfoLevel:
t = "[#00ffff]INFO[white]"
case ErrorLevel:
t = "[red]ERROR[white]"
case DebugLevel:
t = "[yellow]DEBUG[white]"
case FatalLevel:
t = "[red]FATAL[white]"
}
return t + dateStr
}
func (f *UIFormatter) Content(l Level, c string) string {
return c
}
// Timestamp returns the log level with a matching color to the log type
func (f *TerminalFormatter) Timestamp(l Level, d time.Time) string {
t := ""
dateStr := "[" + d.Format(f.format) + "] "
dateStr := "[" + d.Format(f.format) + "] "
switch l {
case InfoLevel:
t = f.output("INFO", skittles.Cyan)
+5 -5
View File
@@ -7,8 +7,8 @@ import "sync"
var SharedWriteManager = NewWriteManager()
type writeData struct {
writeFunc func([]byte)
data []byte
target LogOutput
data []byte
}
// WriteManager is a logging service that handles managing multiple writing streams
@@ -31,9 +31,9 @@ func NewWriteManager() OutputManager {
}
// Append adds a message to the writer runloop
func (m *WriteManager) Append(data []byte, callback func([]byte)) {
func (m *WriteManager) Append(data []byte, target LogOutput) {
m.wg.Add(1)
m.writeChan <- writeData{data: data, writeFunc: callback}
m.writeChan <- writeData{data: data, target: target}
}
// Shutdown stops the sync manager service
@@ -49,7 +49,7 @@ func (m *WriteManager) run() {
select {
case event, ok := <-m.writeChan:
if ok {
event.writeFunc(event.data)
event.target.WriteLogLine(event.data)
m.wg.Done()
}
case <-m.shutdown:
+8 -2
View File
@@ -6,13 +6,19 @@ import (
"github.com/stretchr/testify/assert"
)
type outputFunc func(b []byte)
func (f outputFunc) WriteLogLine(data []byte) {
f(data)
}
func TestWriteManger(t *testing.T) {
testData := []byte(string("hello Austin, how are you doing?"))
waitChan := make(chan []byte)
m := NewWriteManager()
m.Append(testData, func(b []byte) {
m.Append(testData, outputFunc(func(b []byte) {
waitChan <- b
})
}))
resp := <-waitChan
assert.Equal(t, testData, resp)
}
+1 -1
View File
@@ -10,7 +10,7 @@ func NewMockWriteManager() OutputManager {
}
// Append is a mock stub
func (m *MockWriteManager) Append(data []byte, callback func([]byte)) {
func (m *MockWriteManager) Append(data []byte, target LogOutput) {
}
// Shutdown is a mock stub
+58 -33
View File
@@ -10,9 +10,13 @@ import (
// provided for testing
var osExit = os.Exit
type LogOutput interface {
WriteLogLine([]byte)
}
// OutputManager is used to sync data of Output
type OutputManager interface {
Append([]byte, func([]byte))
Append([]byte, LogOutput)
Shutdown()
}
@@ -27,6 +31,8 @@ type Service interface {
Infof(format string, args ...interface{})
Debugf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Add(writer io.Writer, formatter Formatter, levels ...Level)
}
type sourceGroup struct {
@@ -35,38 +41,66 @@ type sourceGroup struct {
levelsSupported []Level
}
// OutputWriter is the standard logging implementation
type OutputWriter struct {
groups []sourceGroup
syncWriter OutputManager
func (s *sourceGroup) WriteLogLine(data []byte) {
_, _ = s.writer.Write(data)
}
// NewOutputWriter create a new logger
func (s *sourceGroup) supportsLevel(l Level) bool {
for _, level := range s.levelsSupported {
if l == level {
return true
}
}
return false
}
// OutputWriter is the standard logging implementation
type OutputWriter struct {
groups []*sourceGroup
syncWriter OutputManager
minLevel Level
}
// NewOutputWriter creates a new logger
func NewOutputWriter(syncWriter OutputManager) *OutputWriter {
return &OutputWriter{
syncWriter: syncWriter,
groups: make([]sourceGroup, 0),
groups: nil,
minLevel: FatalLevel,
}
}
// Add a writer and formatter to output to
func (s *OutputWriter) Add(writer io.Writer, formatter Formatter, levels ...Level) {
s.groups = append(s.groups, sourceGroup{writer: writer, formatter: formatter, levelsSupported: levels})
s.groups = append(s.groups, &sourceGroup{writer: writer, formatter: formatter, levelsSupported: levels})
// track most verbose (lowest) level we need to output
for _, level := range levels {
if level < s.minLevel {
s.minLevel = level
}
}
}
// Error writes an error to the logging sources
func (s *OutputWriter) Error(message string) {
s.output(ErrorLevel, message)
if s.minLevel <= ErrorLevel {
s.output(ErrorLevel, message)
}
}
// Info writes an info string to the logging sources
func (s *OutputWriter) Info(message string) {
s.output(InfoLevel, message)
if s.minLevel <= InfoLevel {
s.output(InfoLevel, message)
}
}
// Debug writes a debug string to the logging sources
func (s *OutputWriter) Debug(message string) {
s.output(DebugLevel, message)
if s.minLevel <= DebugLevel {
s.output(DebugLevel, message)
}
}
// Fatal writes a error string to the logging sources and runs does an os.exit()
@@ -78,17 +112,23 @@ func (s *OutputWriter) Fatal(message string) {
// Errorf writes a formatted error to the logging sources
func (s *OutputWriter) Errorf(format string, args ...interface{}) {
s.output(ErrorLevel, fmt.Sprintf(format, args...))
if s.minLevel <= ErrorLevel {
s.output(ErrorLevel, fmt.Sprintf(format, args...))
}
}
// Infof writes a formatted info statement to the logging sources
func (s *OutputWriter) Infof(format string, args ...interface{}) {
s.output(InfoLevel, fmt.Sprintf(format, args...))
if s.minLevel <= InfoLevel {
s.output(InfoLevel, fmt.Sprintf(format, args...))
}
}
// Debugf writes a formatted debug statement to the logging sources
func (s *OutputWriter) Debugf(format string, args ...interface{}) {
s.output(DebugLevel, fmt.Sprintf(format, args...))
if s.minLevel <= DebugLevel {
s.output(DebugLevel, fmt.Sprintf(format, args...))
}
}
// Fatalf writes a writes a formatted error statement and runs does an os.exit()
@@ -100,31 +140,16 @@ func (s *OutputWriter) Fatalf(format string, args ...interface{}) {
// output does the actual write to the sync manager
func (s *OutputWriter) output(l Level, content string) {
now := time.Now()
for _, group := range s.groups {
if isSupported(group, l) {
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, time.Now()),
if group.supportsLevel(l) {
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, now),
group.formatter.Content(l, content))
s.append(group, []byte(logLine))
s.syncWriter.Append([]byte(logLine), group)
}
}
}
func (s *OutputWriter) append(group sourceGroup, logLine []byte) {
s.syncWriter.Append(logLine, func(b []byte) {
group.writer.Write(b)
})
}
// isSupported checks if the log level is supported
func isSupported(group sourceGroup, l Level) bool {
for _, level := range group.levelsSupported {
if l == level {
return true
}
}
return false
}
// Write implements io.Writer to support SetOutput of the log package
func (s *OutputWriter) Write(p []byte) (n int, err error) {
s.Info(string(p))
+2
View File
@@ -55,6 +55,8 @@ func TestOutputWrite(t *testing.T) {
logger := NewOutputWriter(m)
logger.Add(&testBuffer, f, InfoLevel)
logger.Debugf("debug message not logged here")
testData := "hello Bob Bork, how are you doing?"
logger.Info(testData)
testTime := f.Timestamp(InfoLevel, time.Now())
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
TARGET_DIRECTORY=".build"
BINARY_NAME="cloudflared"
VERSION=$(git describe --tags --always --dirty="-dev")
PRODUCT="cloudflared"
echo "building cloudflared"
make cloudflared
echo "creating build directory"
mkdir ${TARGET_DIRECTORY}
mkdir ${TARGET_DIRECTORY}/contents
cp -r .mac_resources/scripts ${TARGET_DIRECTORY}/scripts
echo "move cloudflared into the build directory"
mv $BINARY_NAME {$TARGET_DIRECTORY}/contents/${PRODUCT}
echo "build the installer package"
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PRODUCT}.pkg
# TODO: our iOS/Mac account doesn't have this installer certificate type.
# need to find how we can get it --sign "Developer ID Installer: Cloudflare" \
echo "cleaning up the build directory"
rm -rf $TARGET_DIRECTORY
+8
View File
@@ -97,3 +97,11 @@ func (b BackoffHandler) GetBaseTime() time.Duration {
func (b *BackoffHandler) Retries() int {
return int(b.retries)
}
func (b *BackoffHandler) ReachedMaxRetries() bool {
return b.retries == b.MaxRetries
}
func (b *BackoffHandler) resetNow() {
b.resetDeadline = time.Now()
}
+36 -529
View File
@@ -1,565 +1,72 @@
package origin
import (
"sync"
"time"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/connection"
"github.com/prometheus/client_golang/prometheus"
)
const (
metricsNamespace = "cloudflared"
tunnelSubsystem = "tunnel"
muxerSubsystem = "muxer"
)
// Metrics uses connection.MetricsNamespace(aka cloudflared) as namespace and connection.TunnelSubsystem
// (tunnel) as subsystem to keep them consistent with the previous qualifier.
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 TunnelMetrics struct {
haConnections prometheus.Gauge
activeStreams prometheus.Gauge
totalRequests prometheus.Counter
requestsPerTunnel *prometheus.CounterVec
// concurrentRequestsLock is a mutex for concurrentRequests and maxConcurrentRequests
concurrentRequestsLock sync.Mutex
concurrentRequestsPerTunnel *prometheus.GaugeVec
// concurrentRequests records count of concurrent requests for each tunnel
concurrentRequests map[string]uint64
maxConcurrentRequestsPerTunnel *prometheus.GaugeVec
// concurrentRequests records max count of concurrent requests for each tunnel
maxConcurrentRequests map[string]uint64
timerRetries prometheus.Gauge
responseByCode *prometheus.CounterVec
responseCodePerTunnel *prometheus.CounterVec
serverLocations *prometheus.GaugeVec
// locationLock is a mutex for oldServerLocations
locationLock sync.Mutex
// oldServerLocations stores the last server the tunnel was connected to
oldServerLocations map[string]string
regSuccess *prometheus.CounterVec
regFail *prometheus.CounterVec
authSuccess prometheus.Counter
authFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA
userHostnamesCounts *prometheus.CounterVec
}
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 NewTunnelMetrics() *TunnelMetrics {
haConnections := prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "ha_connections",
Help: "Number of active ha connections",
})
prometheus.MustRegister(haConnections)
activeStreams := h2mux.NewActiveStreamsMetrics(metricsNamespace, tunnelSubsystem)
totalRequests := prometheus.NewCounter(
var (
totalRequests = prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Namespace: connection.MetricsNamespace,
Subsystem: connection.TunnelSubsystem,
Name: "total_requests",
Help: "Amount of requests proxied through all the tunnels",
})
prometheus.MustRegister(totalRequests)
requestsPerTunnel := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "requests_per_tunnel",
Help: "Amount of requests proxied through each tunnel",
},
[]string{"connection_id"},
)
prometheus.MustRegister(requestsPerTunnel)
concurrentRequestsPerTunnel := prometheus.NewGaugeVec(
concurrentRequests = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Namespace: connection.MetricsNamespace,
Subsystem: connection.TunnelSubsystem,
Name: "concurrent_requests_per_tunnel",
Help: "Concurrent requests proxied through each tunnel",
},
[]string{"connection_id"},
)
prometheus.MustRegister(concurrentRequestsPerTunnel)
maxConcurrentRequestsPerTunnel := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "max_concurrent_requests_per_tunnel",
Help: "Largest number of concurrent requests proxied through each tunnel so far",
},
[]string{"connection_id"},
)
prometheus.MustRegister(maxConcurrentRequestsPerTunnel)
timerRetries := prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "timer_retries",
Help: "Unacknowledged heart beats count",
})
prometheus.MustRegister(timerRetries)
responseByCode := prometheus.NewCounterVec(
responseByCode = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Namespace: connection.MetricsNamespace,
Subsystem: connection.TunnelSubsystem,
Name: "response_by_code",
Help: "Count of responses by HTTP status code",
},
[]string{"status_code"},
)
prometheus.MustRegister(responseByCode)
responseCodePerTunnel := prometheus.NewCounterVec(
requestErrors = prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "response_code_per_tunnel",
Help: "Count of responses by HTTP status code fore each tunnel",
Namespace: connection.MetricsNamespace,
Subsystem: connection.TunnelSubsystem,
Name: "request_errors",
Help: "Count of error proxying to origin",
},
[]string{"connection_id", "status_code"},
)
prometheus.MustRegister(responseCodePerTunnel)
serverLocations := prometheus.NewGaugeVec(
haConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "server_locations",
Help: "Where each tunnel is connected to. 1 means current location, 0 means previous locations.",
},
[]string{"connection_id", "location"},
)
prometheus.MustRegister(serverLocations)
rpcFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_rpc_fail",
Help: "Count of RPC connection errors by type",
},
[]string{"error", "rpcName"},
)
prometheus.MustRegister(rpcFail)
registerFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_register_fail",
Help: "Count of tunnel registration errors by type",
},
[]string{"error", "rpcName"},
)
prometheus.MustRegister(registerFail)
userHostnamesCounts := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "user_hostnames_counts",
Help: "Which user hostnames cloudflared is serving",
},
[]string{"userHostname"},
)
prometheus.MustRegister(userHostnamesCounts)
registerSuccess := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_register_success",
Help: "Count of successful tunnel registrations",
},
[]string{"rpcName"},
)
prometheus.MustRegister(registerSuccess)
authSuccess := prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_success",
Help: "Count of successful tunnel authenticate",
Namespace: connection.MetricsNamespace,
Subsystem: connection.TunnelSubsystem,
Name: "ha_connections",
Help: "Number of active ha connections",
},
)
prometheus.MustRegister(authSuccess)
)
authFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_fail",
Help: "Count of tunnel authenticate errors by type",
},
[]string{"error"},
func init() {
prometheus.MustRegister(
totalRequests,
concurrentRequests,
responseByCode,
requestErrors,
haConnections,
)
prometheus.MustRegister(authFail)
return &TunnelMetrics{
haConnections: haConnections,
activeStreams: activeStreams,
totalRequests: totalRequests,
requestsPerTunnel: requestsPerTunnel,
concurrentRequestsPerTunnel: concurrentRequestsPerTunnel,
concurrentRequests: make(map[string]uint64),
maxConcurrentRequestsPerTunnel: maxConcurrentRequestsPerTunnel,
maxConcurrentRequests: make(map[string]uint64),
timerRetries: timerRetries,
responseByCode: responseByCode,
responseCodePerTunnel: responseCodePerTunnel,
serverLocations: serverLocations,
oldServerLocations: make(map[string]string),
muxerMetrics: newMuxerMetrics(),
tunnelsHA: NewTunnelsForHA(),
regSuccess: registerSuccess,
regFail: registerFail,
rpcFail: rpcFail,
userHostnamesCounts: userHostnamesCounts,
authSuccess: authSuccess,
authFail: authFail,
}
}
func (t *TunnelMetrics) incrementHaConnections() {
t.haConnections.Inc()
func incrementRequests() {
totalRequests.Inc()
concurrentRequests.Inc()
}
func (t *TunnelMetrics) decrementHaConnections() {
t.haConnections.Dec()
}
func (t *TunnelMetrics) updateMuxerMetrics(connectionID string, metrics *h2mux.MuxerMetrics) {
t.muxerMetrics.update(connectionID, metrics)
}
func (t *TunnelMetrics) incrementRequests(connectionID string) {
t.concurrentRequestsLock.Lock()
var concurrentRequests uint64
var ok bool
if concurrentRequests, ok = t.concurrentRequests[connectionID]; ok {
t.concurrentRequests[connectionID]++
concurrentRequests++
} else {
t.concurrentRequests[connectionID] = 1
concurrentRequests = 1
}
if maxConcurrentRequests, ok := t.maxConcurrentRequests[connectionID]; (ok && maxConcurrentRequests < concurrentRequests) || !ok {
t.maxConcurrentRequests[connectionID] = concurrentRequests
t.maxConcurrentRequestsPerTunnel.WithLabelValues(connectionID).Set(float64(concurrentRequests))
}
t.concurrentRequestsLock.Unlock()
t.totalRequests.Inc()
t.requestsPerTunnel.WithLabelValues(connectionID).Inc()
t.concurrentRequestsPerTunnel.WithLabelValues(connectionID).Inc()
}
func (t *TunnelMetrics) decrementConcurrentRequests(connectionID string) {
t.concurrentRequestsLock.Lock()
if _, ok := t.concurrentRequests[connectionID]; ok {
t.concurrentRequests[connectionID]--
}
t.concurrentRequestsLock.Unlock()
t.concurrentRequestsPerTunnel.WithLabelValues(connectionID).Dec()
}
func (t *TunnelMetrics) incrementResponses(connectionID, code string) {
t.responseByCode.WithLabelValues(code).Inc()
t.responseCodePerTunnel.WithLabelValues(connectionID, code).Inc()
}
func (t *TunnelMetrics) registerServerLocation(connectionID, loc string) {
t.locationLock.Lock()
defer t.locationLock.Unlock()
if oldLoc, ok := t.oldServerLocations[connectionID]; ok && oldLoc == loc {
return
} else if ok {
t.serverLocations.WithLabelValues(connectionID, oldLoc).Dec()
}
t.serverLocations.WithLabelValues(connectionID, loc).Inc()
t.oldServerLocations[connectionID] = loc
func decrementConcurrentRequests() {
concurrentRequests.Dec()
}
-121
View File
@@ -1,121 +0,0 @@
package origin
import (
"strconv"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
// can only be called once
var m = NewTunnelMetrics()
func TestConcurrentRequestsSingleTunnel(t *testing.T) {
routines := 20
var wg sync.WaitGroup
wg.Add(routines)
for i := 0; i < routines; i++ {
go func() {
m.incrementRequests("0")
wg.Done()
}()
}
wg.Wait()
assert.Len(t, m.concurrentRequests, 1)
assert.Equal(t, uint64(routines), m.concurrentRequests["0"])
assert.Len(t, m.maxConcurrentRequests, 1)
assert.Equal(t, uint64(routines), m.maxConcurrentRequests["0"])
wg.Add(routines / 2)
for i := 0; i < routines/2; i++ {
go func() {
m.decrementConcurrentRequests("0")
wg.Done()
}()
}
wg.Wait()
assert.Equal(t, uint64(routines-routines/2), m.concurrentRequests["0"])
assert.Equal(t, uint64(routines), m.maxConcurrentRequests["0"])
}
func TestConcurrentRequestsMultiTunnel(t *testing.T) {
m.concurrentRequests = make(map[string]uint64)
m.maxConcurrentRequests = make(map[string]uint64)
tunnels := 20
var wg sync.WaitGroup
wg.Add(tunnels)
for i := 0; i < tunnels; i++ {
go func(i int) {
// if we have j < i, then tunnel 0 won't have a chance to call incrementRequests
for j := 0; j < i+1; j++ {
id := strconv.Itoa(i)
m.incrementRequests(id)
}
wg.Done()
}(i)
}
wg.Wait()
assert.Len(t, m.concurrentRequests, tunnels)
assert.Len(t, m.maxConcurrentRequests, tunnels)
for i := 0; i < tunnels; i++ {
id := strconv.Itoa(i)
assert.Equal(t, uint64(i+1), m.concurrentRequests[id])
assert.Equal(t, uint64(i+1), m.maxConcurrentRequests[id])
}
wg.Add(tunnels)
for i := 0; i < tunnels; i++ {
go func(i int) {
for j := 0; j < i+1; j++ {
id := strconv.Itoa(i)
m.decrementConcurrentRequests(id)
}
wg.Done()
}(i)
}
wg.Wait()
assert.Len(t, m.concurrentRequests, tunnels)
assert.Len(t, m.maxConcurrentRequests, tunnels)
for i := 0; i < tunnels; i++ {
id := strconv.Itoa(i)
assert.Equal(t, uint64(0), m.concurrentRequests[id])
assert.Equal(t, uint64(i+1), m.maxConcurrentRequests[id])
}
}
func TestRegisterServerLocation(t *testing.T) {
tunnels := 20
var wg sync.WaitGroup
wg.Add(tunnels)
for i := 0; i < tunnels; i++ {
go func(i int) {
id := strconv.Itoa(i)
m.registerServerLocation(id, "LHR")
wg.Done()
}(i)
}
wg.Wait()
for i := 0; i < tunnels; i++ {
id := strconv.Itoa(i)
assert.Equal(t, "LHR", m.oldServerLocations[id])
}
wg.Add(tunnels)
for i := 0; i < tunnels; i++ {
go func(i int) {
id := strconv.Itoa(i)
m.registerServerLocation(id, "AUS")
wg.Done()
}(i)
}
wg.Wait()
for i := 0; i < tunnels; i++ {
id := strconv.Itoa(i)
assert.Equal(t, "AUS", m.oldServerLocations[id])
}
}

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