mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 588f1a03c4 | |||
| f8fbbcd806 | |||
| 2ca4633f89 | |||
| 2ce11a20c4 | |||
| ff7c48568c | |||
| 958650be1f | |||
| eb51ff0a6d | |||
| 3f4407ce27 | |||
| d9636c73b4 | |||
| 6cbf90883d | |||
| 76ade67f95 | |||
| 997f2cf612 | |||
| ceb509ee98 | |||
| 5a3c0fdffa | |||
| 2822fbe3db | |||
| 5148d00516 | |||
| bb10e1dee5 | |||
| e445fd92f7 | |||
| bccf4a63dc | |||
| 7059ef8e13 | |||
| cbdf88ea28 | |||
| 79ebfa8304 | |||
| 5a5e49179a | |||
| 470a85e65d | |||
| d7da74cb9e | |||
| 27e1277a3b | |||
| 6238fd9022 | |||
| f985ed567f | |||
| d54c8cc745 | |||
| 548e85829a | |||
| fd14bf440b | |||
| e2b18364f4 | |||
| 52c4f875fa | |||
| dac077fef1 | |||
| 0e68b1d24e | |||
| 6968b714d0 | |||
| 9310241e1a | |||
| 1cb22817db | |||
| fe4b9b1535 | |||
| d04f48d872 | |||
| b5f6559179 | |||
| 89d408e3bd | |||
| 533d005159 | |||
| 98c3957d30 | |||
| 671754fd19 | |||
| d1801776b0 | |||
| 11c06b5a1f | |||
| 27cd83c2d3 | |||
| 75bdc96763 | |||
| c51879b17f | |||
| 738b4f8d25 | |||
| c7a44009d3 | |||
| 754ad59512 | |||
| 414cb12f02 | |||
| d0a1daac3b | |||
| 2afa307765 | |||
| a4a9f45b0a | |||
| 1da4fbbe0b | |||
| 836149a5b0 | |||
| 4ff215ee8c | |||
| 17e3073a17 | |||
| 071d595371 | |||
| b6c85401a5 | |||
| 3ef3e7a99a | |||
| 12ad264eb3 | |||
| 1082ac1c36 | |||
| a233f975c1 | |||
| 5f6e867685 | |||
| 78a9454023 | |||
| ca85df10ff | |||
| b8333b44a2 | |||
| e49a7a4389 | |||
| 5749425671 | |||
| fac9dfb6e5 | |||
| d9ec18314d | |||
| ed024d0741 | |||
| fd4000184c | |||
| bcc847963e | |||
| 1e8dea9112 | |||
| cd4af5696d | |||
| 8527d03a29 | |||
| fa8aa02270 | |||
| aa24338225 | |||
| 0924549efd | |||
| 67a3be5b7a | |||
| c33a516663 | |||
| ed1389ef08 | |||
| 8fb6508ae6 | |||
| ee8c8bd4c6 | |||
| 0269062b24 | |||
| bccf650165 | |||
| cecc74d3fb | |||
| 18992efa0c | |||
| bd8af7d80d | |||
| dff694b218 | |||
| 38af26e232 | |||
| 8f3526289a | |||
| 81dff44bb9 | |||
| 6e45e0d53b | |||
| a7d2de1e12 | |||
| 59cae0f622 | |||
| d678584d89 | |||
| f1b57526b3 | |||
| 3eb9efd9f0 | |||
| 8d99e92852 | |||
| 31ff7caeeb | |||
| 5d84874d53 | |||
| b06fe0fc5f | |||
| bf38e5aa12 | |||
| bf068e728f | |||
| f88732277a | |||
| a11c64b091 | |||
| e404c29edb | |||
| 98a0844f56 | |||
| d6d65d183b | |||
| 951d13d76c | |||
| f99ae90ca1 | |||
| e99d0b5bc4 | |||
| 48c5721bc6 | |||
| 235897ba21 | |||
| bf737a0efc | |||
| 4c5ebccacc | |||
| a3153c6add | |||
| a2bbe1bdc2 | |||
| 6526211a69 | |||
| 103b2eca00 | |||
| 14ea6298bc | |||
| b297e8bb90 |
@@ -1 +0,0 @@
|
||||
.git
|
||||
@@ -4,7 +4,7 @@ jobs:
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.15.x]
|
||||
go-version: [1.17.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install go-sumtype
|
||||
run: go get github.com/BurntSushi/go-sumtype
|
||||
run: go get github.com/sudarshan-reddy/go-sumtype
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Test
|
||||
|
||||
@@ -15,3 +15,4 @@ cscope.*
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
/.cover
|
||||
built_artifacts/
|
||||
|
||||
Vendored
+3
-3
@@ -46,8 +46,8 @@ git reset --hard origin/master
|
||||
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
|
||||
tee cloudflared.rb <<EOF
|
||||
class Cloudflared < Formula
|
||||
desc 'Argo Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/argo-tunnel/'
|
||||
desc 'Cloudflare Tunnel'
|
||||
homepage 'https://developers.cloudflare.com/cloudflare-one/connections/connect-apps'
|
||||
url '$URL'
|
||||
sha256 '$SHA256'
|
||||
version '$VERSION'
|
||||
@@ -62,6 +62,6 @@ git add cloudflared.rb
|
||||
git diff
|
||||
git config user.name "cloudflare-warp-bot"
|
||||
git config user.email "warp-bot@cloudflare.com"
|
||||
git commit -m "Release Argo Tunnel $VERSION"
|
||||
git commit -m "Release Cloudflare Tunnel $VERSION"
|
||||
|
||||
git push -v origin master
|
||||
|
||||
+62
-1
@@ -1,6 +1,67 @@
|
||||
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
|
||||
|
||||
## 2020.5.0
|
||||
## 2021.9.2
|
||||
|
||||
### New features
|
||||
- `cloudflared` can now run with `quic` as the underlying tunnel transport protocol. To try it, change or add "protocol: quic" to your config.yml file or
|
||||
run cloudflared with the `--protocol quic` flag. e.g:
|
||||
`cloudflared tunnel --protocol quic run <tunnel-name>`
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed some generic transport bugs in `quic` mode. It's advised to upgrade to at least this version (2021.9.2) when running `cloudflared`
|
||||
with `quic` protocol.
|
||||
- `cloudflared` docker images will now show version.
|
||||
|
||||
|
||||
## 2021.8.4
|
||||
### Improvements
|
||||
- Temporary tunnels (those hosted on trycloudflare.com that do not require a Cloudflare login) now run as Named Tunnels
|
||||
underneath. We recall that these tunnels should not be relied upon for production usage as they come with no guarantee
|
||||
of uptime. Previous cloudflared versions will soon be unable to run legacy temporary tunnels and will require an update
|
||||
(to this version or more recent).
|
||||
|
||||
## 2021.8.2
|
||||
### Improvements
|
||||
- Because Equinox os shutting down, all cloudflared releases are now present [here](https://github.com/cloudflare/cloudflared/releases).
|
||||
[Equinox](https://dl.equinox.io/cloudflare/cloudflared/stable) will no longer receive updates.
|
||||
|
||||
## 2021.8.0
|
||||
### Bug fixes
|
||||
- Prevents tunnel from accidentally running when only proxy-dns should run.
|
||||
|
||||
### Improvements
|
||||
- If auto protocol transport lookup fails, we now default to a transport instead of not connecting.
|
||||
|
||||
## 2021.6.0
|
||||
### Bug Fixes
|
||||
- Fixes a http2 transport (the new default for Named Tunnels) to work with unix socket origins.
|
||||
|
||||
|
||||
## 2021.5.10
|
||||
### Bug Fixes
|
||||
- Fixes a memory leak in h2mux transport that connects cloudflared to Cloudflare edge.
|
||||
|
||||
|
||||
## 2021.5.9
|
||||
### New Features
|
||||
- Uses new Worker based login helper service to facilitate token exchange in cloudflared flows.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixes Centos-7 builds.
|
||||
|
||||
## 2021.5.8
|
||||
### New Features
|
||||
- When creating a DNS record to point a hostname at a tunnel, you can now use --overwrite-dns to overwrite any existing
|
||||
DNS records with that hostname. This works both when using the CLI to provision DNS, as well as when starting an adhoc
|
||||
named tunnel, e.g.:
|
||||
- `cloudflared tunnel route dns --overwrite-dns foo-tunnel foo.example.com`
|
||||
- `cloudflared tunnel --overwrite-dns --name foo-tunnel --hostname foo.example.com`
|
||||
|
||||
## 2021.5.7
|
||||
### New Features
|
||||
- Named Tunnels will automatically select the protocol to connect to Cloudflare's edge network.
|
||||
|
||||
## 2021.5.0
|
||||
|
||||
### New Features
|
||||
- It is now possible to run the same tunnel using more than one `cloudflared` instance. This is a server-side change and
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.16.4 as builder
|
||||
FROM golang:1.17.1 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
TARGET_GOOS=${TARGET_GOOS} \
|
||||
|
||||
@@ -11,6 +11,13 @@ ifneq ($(GO_BUILD_TAGS),)
|
||||
GO_BUILD_TAGS := -tags $(GO_BUILD_TAGS)
|
||||
endif
|
||||
|
||||
ifeq ($(NIGHTLY), true)
|
||||
DEB_PACKAGE_NAME := cloudflared-nightly
|
||||
NIGHTLY_FLAGS := --conflicts cloudflared --replaces cloudflared
|
||||
else
|
||||
DEB_PACKAGE_NAME := cloudflared
|
||||
endif
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
|
||||
|
||||
@@ -19,16 +26,6 @@ PACKAGE_DIR := $(CURDIR)/packaging
|
||||
INSTALL_BINDIR := /usr/bin/
|
||||
MAN_DIR := /usr/share/man/man1/
|
||||
|
||||
EQUINOX_FLAGS = --version="$(VERSION)" \
|
||||
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
|
||||
--app="$(EQUINOX_APP_ID)" \
|
||||
--token="$(EQUINOX_TOKEN)" \
|
||||
--channel="$(EQUINOX_CHANNEL)"
|
||||
|
||||
ifeq ($(EQUINOX_IS_DRAFT), true)
|
||||
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
|
||||
endif
|
||||
|
||||
LOCAL_ARCH ?= $(shell uname -m)
|
||||
ifneq ($(GOARCH),)
|
||||
TARGET_ARCH ?= $(GOARCH)
|
||||
@@ -42,6 +39,8 @@ else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(LOCAL_ARCH),aarch64)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(LOCAL_ARCH),arm64)
|
||||
TARGET_ARCH ?= arm64
|
||||
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
|
||||
TARGET_ARCH ?= arm
|
||||
else
|
||||
@@ -81,7 +80,7 @@ clean:
|
||||
go clean
|
||||
|
||||
.PHONY: cloudflared
|
||||
cloudflared: tunnel-deps
|
||||
cloudflared:
|
||||
ifeq ($(FIPS), true)
|
||||
$(info Building cloudflared with go-fips)
|
||||
-test -f fips/fips.go && mv fips/fips.go fips/fips.go.linux-amd64
|
||||
@@ -133,12 +132,12 @@ define build_package
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > $(PACKAGE_DIR)/cloudflared.1
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
|
||||
--description 'Cloudflare Argo tunnel daemon' \
|
||||
--description 'Cloudflare Tunnel daemon' \
|
||||
--vendor 'Cloudflare' \
|
||||
--license 'Cloudflare Service Agreement' \
|
||||
--url 'https://github.com/cloudflare/cloudflared' \
|
||||
-m 'Cloudflare <support@cloudflare.com>' \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared --after-install postinst.sh --after-remove postrm.sh \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n $(DEB_PACKAGE_NAME) $(NIGHTLY_FLAGS) --after-install postinst.sh --after-remove postrm.sh \
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(MAN_DIR)
|
||||
endef
|
||||
|
||||
@@ -150,6 +149,14 @@ cloudflared-deb: cloudflared
|
||||
cloudflared-rpm: cloudflared
|
||||
$(call build_package,rpm)
|
||||
|
||||
.PHONY: cloudflared-pkg
|
||||
cloudflared-pkg: cloudflared
|
||||
$(call build_package,osxpkg)
|
||||
|
||||
.PHONY: cloudflared-msi
|
||||
cloudflared-msi: cloudflared
|
||||
wixl --define Version=$(VERSION) --define Path=$(EXECUTABLE_PATH) --output cloudflared-$(VERSION)-$(TARGET_ARCH).msi cloudflared.wxs
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
cloudflared-darwin-amd64.tgz: cloudflared
|
||||
tar czf cloudflared-darwin-amd64.tgz cloudflared
|
||||
@@ -216,14 +223,14 @@ homebrew-upload: cloudflared-darwin-amd64.tgz
|
||||
homebrew-release: homebrew-upload
|
||||
./publish-homebrew-formula.sh cloudflared-darwin-amd64.tgz $(VERSION) homebrew-cloudflare
|
||||
|
||||
.PHONY: release
|
||||
release: bin/equinox
|
||||
bin/equinox release $(EQUINOX_FLAGS) -- $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
|
||||
|
||||
.PHONY: github-release
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-release-built-pkgs
|
||||
github-release-built-pkgs:
|
||||
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-message
|
||||
github-message:
|
||||
python3 github_message.py --release-version $(VERSION)
|
||||
@@ -233,28 +240,27 @@ 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/
|
||||
|
||||
.PHONY: tunnel-deps
|
||||
tunnel-deps: tunnelrpc/tunnelrpc.capnp.go
|
||||
|
||||
tunnelrpc/tunnelrpc.capnp.go: tunnelrpc/tunnelrpc.capnp
|
||||
.PHONY: tunnelrpc-deps
|
||||
tunnelrpc-deps:
|
||||
which capnp # https://capnproto.org/install.html
|
||||
which capnpc-go # go get zombiezen.com/go/capnproto2/capnpc-go
|
||||
capnp compile -ogo tunnelrpc/tunnelrpc.capnp
|
||||
|
||||
.PHONY: quic-deps
|
||||
quic-deps:
|
||||
which capnp
|
||||
which capnpc-go
|
||||
capnp compile -ogo quic/schema/quic_metadata_protocol.capnp
|
||||
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet -mod=vendor ./...
|
||||
which go-sumtype # go get github.com/BurntSushi/go-sumtype (don't do this in build directory or this will cause vendor issues)
|
||||
# go get github.com/sudarshan-reddy/go-sumtype (don't do this in build directory or this will cause vendor issues)
|
||||
# Note: If you have github.com/BurntSushi/go-sumtype then you might have to use the repo above instead
|
||||
# for now because it uses an older version of golang.org/x/tools.
|
||||
which go-sumtype
|
||||
go-sumtype $$(go list -mod=vendor ./...)
|
||||
|
||||
.PHONY: msi
|
||||
msi: cloudflared
|
||||
go-msi make --msi cloudflared.msi --version $(MSI_VERSION)
|
||||
|
||||
.PHONY: goimports
|
||||
goimports:
|
||||
for d in $$(go list -mod=readonly -f '{{.Dir}}' -a ./... | fgrep -v tunnelrpc) ; do goimports -format-only -local github.com/cloudflare/cloudflared -w $$d ; done
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
# Argo Tunnel client
|
||||
# Cloudflare Tunnel client
|
||||
|
||||
Contains the command-line client for Cloudflare Tunnel, a tunneling daemon that proxies traffic from the Cloudflare network to your origins.
|
||||
This daemon sits between Cloudflare network and your origin (e.g. a webserver). Cloudflare attracts client requests and sends them to you
|
||||
via this daemon, without requiring you to poke holes on your firewall --- your origin can remain as closed as possible.
|
||||
Extensive documentation can be found in the [Cloudflare Tunnel section](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps) of the Cloudflare Docs.
|
||||
All usages related with proxying to your origins are available under `cloudflared tunnel help`.
|
||||
|
||||
You can also use `cloudflared` to access Tunnel origins (that are protected with `cloudflared tunnel`) for TCP traffic
|
||||
at Layer 4 (i.e., not HTTP/websocket), which is relevant for use cases such as SSH, RDP, etc.
|
||||
Such usages are available under `cloudflared access help`.
|
||||
|
||||
You can instead use [WARP client](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/private-networks)
|
||||
to access private origins behind Tunnels for Layer 4 traffic without requiring `cloudflared access` commands on the client side.
|
||||
|
||||
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/cloudflare-one/connections/connect-apps) of the Cloudflare Docs.
|
||||
|
||||
## Before you get started
|
||||
|
||||
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.
|
||||
|
||||
Before you use Cloudflare Tunnel, you'll need to complete a few steps in the Cloudflare dashboard: you need to add a
|
||||
website to your Cloudflare account. Note that today it is possible to use Tunnel without a website (e.g. for private
|
||||
routing), but for legacy reasons this requirement is still necessary:
|
||||
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.
|
||||
@@ -18,26 +32,29 @@ Downloads are available as standalone binaries, a Docker image, and Debian, RPM,
|
||||
* A Docker image of `cloudflared` is [available on DockerHub](https://hub.docker.com/r/cloudflare/cloudflared)
|
||||
* You can install on Windows machines with the [steps here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#windows)
|
||||
|
||||
User documentation for Argo Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
User documentation for Cloudflare Tunnel can be found at https://developers.cloudflare.com/cloudflare-one/connections/connect-apps
|
||||
|
||||
|
||||
## 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.
|
||||
Once installed, you can authenticate `cloudflared` into your Cloudflare account and begin creating Tunnels to serve traffic to your origins.
|
||||
|
||||
* Create a Tunnel with [these instructions](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/create-tunnel)
|
||||
* Route traffic to that Tunnel with [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns) or with a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb)
|
||||
* Route traffic to that Tunnel:
|
||||
* Via public [DNS records in Cloudflare](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns)
|
||||
* Or via a public hostname guided by a [Cloudflare Load Balancer](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb)
|
||||
* Or from [WARP client private traffic](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/private-networks)
|
||||
|
||||
|
||||
## 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/cloudflare-one/connections/connect-apps/trycloudflare).
|
||||
Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare).
|
||||
|
||||
## Deprecated versions
|
||||
|
||||
Cloudflare currently supports all versions of `cloudflared`. Starting on March 20, 2021, Cloudflare will no longer support versions released prior to 2020.5.1.
|
||||
|
||||
All features available in versions released prior to 2020.5.1 are available in current versions. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
||||
Cloudflare currently supports versions of `cloudflared` 2020.5.1 and later. Breaking changes unrelated to feature availability may be introduced that will impact versions released prior to 2020.5.1. You can read more about upgrading `cloudflared` in our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation#updating-cloudflared).
|
||||
|
||||
| Version(s) | Deprecation status |
|
||||
|---|---|
|
||||
| 2020.5.1 and later | Supported |
|
||||
| Versions prior to 2020.5.1 | Will no longer be supported starting March 20, 2021 |
|
||||
| Versions prior to 2020.5.1 | No longer supported |
|
||||
|
||||
+150
@@ -1,3 +1,153 @@
|
||||
2021.10.4
|
||||
- 2021-10-21 TUN-5287: Fix misuse of wait group in TestQUICServer that caused the test to exit immediately
|
||||
- 2021-10-21 TUN-5286: Upgrade crypto/ssh package to fix CVE-2020-29652
|
||||
- 2021-10-18 TUN-5262: Allow to configure max fetch size for listing queries
|
||||
- 2021-10-19 TUN-5262: Improvements to `max-fetch-size` that allow to deal with large number of tunnels in account
|
||||
- 2021-10-15 TUN-5261: Collect QUIC metrics about RTT, packets and bytes transfered and log events at tracing level
|
||||
- 2021-10-19 TUN-5184: Make sure outstanding websocket write is finished, and no more writes after shutdown
|
||||
|
||||
2021.10.3
|
||||
- 2021-10-14 TUN-5255: Fix potential panic if Cloudflare API fails to respond to GetTunnel(id) during delete command
|
||||
- 2021-10-14 TUN-5257: Fix more cfsetup targets that were broken by recent package changes
|
||||
|
||||
2021.10.2
|
||||
- 2021-10-11 TUN-5138: Switch to QUIC on auto protocol based on threshold
|
||||
- 2021-10-14 TUN-5250: Add missing packages for cfsetup to succeed in github release pkgs target
|
||||
|
||||
2021.10.1
|
||||
- 2021-10-12 TUN-5246: Use protocol: quic for Quick tunnels if one is not already set
|
||||
- 2021-10-13 TUN-5249: Revert "TUN-5138: Switch to QUIC on auto protocol based on threshold"
|
||||
|
||||
2021.10.0
|
||||
- 2021-10-11 TUN-5138: Switch to QUIC on auto protocol based on threshold
|
||||
- 2021-10-07 TUN-5195: Do not set empty body if not applicable
|
||||
- 2021-10-08 UN-5213: Increase MaxStreams value for QUIC transport
|
||||
- 2021-09-28 TUN-5169: Release 2021.9.2 CHANGES.md
|
||||
- 2021-09-28 TUN-5164: Update README and clean up references to Argo Tunnel (using Cloudflare Tunnel instead)
|
||||
|
||||
2021.9.2
|
||||
- 2021-09-21 TUN-5129: Use go 1.17 and copy .git folder to docker build to compute version
|
||||
- 2021-09-21 TUN-5128: Enforce maximum grace period
|
||||
- 2021-09-22 TUN-5141: Make sure websocket pinger returns before streaming returns
|
||||
- 2021-09-24 TUN-5142: Add asynchronous servecontrolstream for QUIC
|
||||
- 2021-09-24 TUN-5142: defer close rpcconn inside unregister instead of ServeControlStream
|
||||
- 2021-09-27 TUN-5160: Set request.ContentLength when this value is in request header
|
||||
|
||||
2021.9.1
|
||||
- 2021-09-21 TUN-5118: Quic connection now detects duplicate connections similar to http2
|
||||
- 2021-09-15 Fix TryCloudflare link
|
||||
|
||||
2021.9.0
|
||||
- 2021-09-02 Fix broken TryCloudflare link
|
||||
- 2021-09-03 Add support for taking named tunnel credentials from an environment variable
|
||||
- 2021-08-30 TUN-5012: Use patched go-sumtype
|
||||
- 2021-08-31 TUN-5011: Use the region parameter in fallback SRV lookup
|
||||
- 2021-08-31 TUN-5029: Do not strip cf- prefixed headers
|
||||
- 2021-08-29 TUN-5009: Updated github action to use go 1.17.x for checks
|
||||
- 2021-08-28 TUN-5010: --region should be a string flag
|
||||
- 2021-08-10 Allow building on arm64 platforms
|
||||
- 2021-06-09 Update README.md
|
||||
- 2021-05-31 🖌️ Allow providing TokenID and TokenSecret as env vars when calling cloudflared access
|
||||
- 2021-05-31 🎨 Prefix env var parameters with TUNNEL
|
||||
|
||||
2021.8.7
|
||||
- 2021-08-28 Revert "TUN-4926: Implement --region configuration option"
|
||||
|
||||
2021.8.6
|
||||
- 2021-08-27 TUN-5000: De-flake logging to dir component test in Windows by increasing to buffer to cope with more logging
|
||||
- 2021-08-27 TUN-5003: Fix cfsetup for non-FIPS golang version
|
||||
|
||||
2021.8.5
|
||||
- 2021-08-27 TUN-4961: Update quic-go to latest
|
||||
- 2021-08-27 Release 2021.8.4
|
||||
|
||||
2021.8.4
|
||||
- 2021-08-26 TUN-4974: Fix regression where we were debug logging by accident
|
||||
- 2021-08-26 TUN-4970: Only default to http2 for warp-routing if protocol is h2mux
|
||||
- 2021-08-26 TUN-4981: Improve readability of prepareTunnelConfig method
|
||||
- 2021-08-26 TUN-4926: Implement --region configuration option
|
||||
- 2021-07-09 TUN-4821: Make quick tunnels the default in cloudflared
|
||||
|
||||
2021.8.3
|
||||
- 2021-08-23 TUN-4889: Add back appendtagheaders function
|
||||
- 2021-08-21 TUN-4940: Fix cloudflared not picking up correct NextProtos for quic
|
||||
- 2021-08-21 TUN-4613: Add a no-op protocol version slot
|
||||
- 2021-08-13 TUN-4922: Downgrade quic-go library to 0.20.0
|
||||
- 2021-08-17 TUN-4866: Add Control Stream for QUIC
|
||||
- 2021-08-17 TUN-4927: Parameterize region in edge discovery code
|
||||
- 2021-08-06 TUN-4602: Added UDP resolves to Edge discovery
|
||||
|
||||
2021.8.2
|
||||
- 2021-08-03 TUN-4597: Added HTTPProxy for QUIC
|
||||
- 2021-08-04 TUN-4795: Remove Equinox releases
|
||||
- 2021-08-09 TUN-4911: Append Environment variable to Path instead of overwriting it
|
||||
|
||||
2021.8.1
|
||||
- 2021-08-02 TUN-4855: Added CHANGES.md for release 2021.8.0
|
||||
- 2021-08-03 TUN-4597: Add a QUIC server skeleton
|
||||
- 2021-08-03 TUN-4873: Disable unix domain socket test for windows unit tests
|
||||
- 2021-08-04 TUN-4875: Added amd64-linux builds back to releases
|
||||
|
||||
2021.8.0
|
||||
- 2021-07-30 TUN-4847: Allow to list tunnels by prefix name or exclusion prefix name
|
||||
- 2021-07-30 TUN-4772: Release built executables with packages
|
||||
- 2021-07-30 TUN-4851: Component tests to smoke test that Proxy DNS and Tunnel are only run when expected
|
||||
- 2021-07-28 TUN-4811: Publish quick tunnels' hostname in /metrics under `userHostname` for backwards-compatibility
|
||||
- 2021-07-29 TUN-4832: Prevent tunnel from running accidentally when only proxy-dns should run
|
||||
- 2021-07-28 TUN-4819: Tolerate protocol TXT record lookup failing
|
||||
|
||||
2021.7.4
|
||||
- 2021-07-28 TUN-4814: Revert "TUN-4699: Make quick tunnels the default in cloudflared"
|
||||
- 2021-07-28 TUN-4812: Disable CGO for cloudflared builds
|
||||
|
||||
2021.7.3
|
||||
- 2021-07-27 TUN-4799: Build deb, msi and rpm packages with fips
|
||||
|
||||
2021.7.2
|
||||
- 2021-07-27 Fixed a syntax error with python logging.
|
||||
|
||||
2021.7.1
|
||||
- 2021-07-21 TUN-4755: Add a windows msi release option to Make
|
||||
- 2021-07-22 TUN-4761: Added a build-all-packages target to cfsetup
|
||||
- 2021-07-26 TUN-4771: Upload deb, rpm and msi packages to github
|
||||
- 2021-07-14 TUN-4714: Name nightly package cloudflared-nightly to avoid apt conflict
|
||||
- 2021-07-16 TUN-4701: Split Proxy into ProxyHTTP and ProxyTCP
|
||||
- 2021-07-08 TUN-4596: Add QUIC application protocol for QUIC stream handshake
|
||||
- 2021-07-09 TUN-4699: Make quick tunnels the default in cloudflared
|
||||
|
||||
2021.7.0
|
||||
- 2021-07-01 TUN-4626: Proxy non-stream based origin websockets with http Roundtrip.
|
||||
- 2021-07-01 TUN-4655: ingress.StreamBasedProxy.EstablishConnection takes dest input
|
||||
- 2021-07-09 TUN-4698: Add cloudflared metrics endpoint to serve quick tunnel hostname
|
||||
- 2021-06-21 TUN-4521: Modify cloudflared to use zoneless-tunnels-worker for free tunnels
|
||||
- 2021-04-05 AUTH-3475: Updated GetAppInfo error message
|
||||
|
||||
2021.6.0
|
||||
- 2021-06-21 TUN-4571: Changelog for 2021.6.0
|
||||
- 2021-06-18 TUN-4571: Fix proxying to unix sockets when using HTTP2 transport to Cloudflare Edge
|
||||
- 2021-06-07 TUN-4502: Make `cloudflared tunnel route` subcommands described consistently
|
||||
- 2021-06-08 TUN-4504: Fix component tests in windows
|
||||
- 2021-05-27 TUN-4461: Log resulting DNS hostname if one is received from Cloudflare API
|
||||
|
||||
2021.5.10
|
||||
- 2021-05-25 TUN-4456: Replaced instances of Tick() with Ticker() in h2mux paths
|
||||
|
||||
2021.5.9
|
||||
- 2021-05-20 TUN-4426: Fix centos builds
|
||||
- 2021-05-20 Update changelog
|
||||
- 2021-04-30 AUTH-3426: Point to new transfer service URL and eliminate PUT /ok
|
||||
|
||||
2021.5.8
|
||||
- 2021-05-14 TUN-4419: Improve error message when cloudflared cannot reach origin
|
||||
- 2021-05-19 TUN-4425: --overwrite-dns flag for in adhoc and route dns cmds
|
||||
|
||||
2021.5.7
|
||||
- 2021-05-17 Fix typo in Changes.md
|
||||
- 2021-05-17 TUN-4421: Named Tunnels will automatically select the protocol to connect to Cloudflare's edge network
|
||||
|
||||
2021.5.6
|
||||
- 2021-05-14 TUN-4418: Downgrade to Go 1.16.3
|
||||
|
||||
2021.5.5
|
||||
|
||||
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
VERSION=$(git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
export CGO_ENABLED=0
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
windowsArchs=("amd64" "386")
|
||||
export TARGET_OS=windows
|
||||
for arch in ${windowsArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-msi
|
||||
mv ./cloudflared.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe
|
||||
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
|
||||
done
|
||||
|
||||
|
||||
export FIPS=true
|
||||
linuxArchs=("amd64" "386" "arm" "arm64")
|
||||
export TARGET_OS=linux
|
||||
for arch in ${linuxArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-deb
|
||||
mv cloudflared\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-linux-$arch.deb
|
||||
|
||||
# rpm packages invert the - and _ and use x86_64 instead of amd64.
|
||||
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g')
|
||||
RPMARCH=$arch
|
||||
if [ $arch == "amd64" ];then
|
||||
RPMARCH="x86_64"
|
||||
fi
|
||||
if [ $arch == "arm64" ]; then
|
||||
RPMARCH="aarch64"
|
||||
fi
|
||||
make cloudflared-rpm
|
||||
mv cloudflared-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-linux-$RPMARCH.rpm
|
||||
|
||||
# finally move the linux binary as well.
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-linux-$arch
|
||||
done
|
||||
|
||||
+82
-144
@@ -1,5 +1,5 @@
|
||||
pinned_go: &pinned_go go=1.16.4-1
|
||||
pinned_go_fips: &pinned_go_fips go-fips=1.15.5-3
|
||||
pinned_go: &pinned_go go=1.17-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.16.6-6
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: buster
|
||||
@@ -14,9 +14,63 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
build-deb:
|
||||
build-non-fips: # helpful to catch problems with non-fips (only used for releasing non-linux artifacts) before releases
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
build-all-packages: #except osxpkg
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
pre-cache:
|
||||
# TODO: https://jira.cfops.it/browse/TUN-4792 Replace this wixl with the official one once msitools supports
|
||||
# environment.
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
post-cache:
|
||||
- export FIPS=true
|
||||
- ./build-packages.sh
|
||||
github-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
- wget
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache:
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
# build all packages and move them to /cfsetup/built_artifacts
|
||||
- ./build-packages.sh
|
||||
# release the packages built and moved to /cfsetup/built_artifacts
|
||||
- make github-release-built-pkgs
|
||||
build-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deb_deps
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
@@ -26,13 +80,18 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared-deb
|
||||
build-deb-nightly:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export NIGHTLY=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
builddeps: *build_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
@@ -50,143 +109,17 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make publish-deb
|
||||
release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make release
|
||||
github-release-linux-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make github-release
|
||||
release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make release
|
||||
github-release-linux-armv6:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- gcc-arm-linux-gnueabihf
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm
|
||||
- export CC=arm-linux-gnueabihf-gcc
|
||||
- make github-release
|
||||
release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make release
|
||||
github-release-linux-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-multilib
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=386
|
||||
- make github-release
|
||||
release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make release
|
||||
github-release-windows-amd64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=amd64
|
||||
- export CC=x86_64-w64-mingw32-gcc
|
||||
- make github-release
|
||||
release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
post-cache:
|
||||
- export GOOS=windows
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make release
|
||||
github-release-windows-386:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- gcc-mingw-w64
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
post-cache:
|
||||
- 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
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
- make github-mac-upload
|
||||
test:
|
||||
@@ -197,7 +130,7 @@ stretch: &stretch
|
||||
- gotest-to-teamcity
|
||||
pre-cache:
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- go get github.com/BurntSushi/go-sumtype
|
||||
- go get github.com/sudarshan-reddy/go-sumtype@v0.0.0-20210827105221-82eca7e5abb1
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
@@ -235,6 +168,9 @@ stretch: &stretch
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: *install_pygithub
|
||||
@@ -279,8 +215,9 @@ centos-7:
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.16.4.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.4.linux-amd64.tar.gz
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://golang.org/dl/go1.16.3.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.3.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
@@ -291,8 +228,9 @@ centos-7:
|
||||
builddeps: *el7_builddeps
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.16.4.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.4.linux-amd64.tar.gz
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://golang.org/dl/go1.16.3.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.3.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<?if $(var.Platform)="x86"?>
|
||||
<?define Program_Files="ProgramFilesFolder"?>
|
||||
<?else?>
|
||||
<?define Program_Files="ProgramFiles64Folder"?>
|
||||
<?endif?>
|
||||
<?ifndef var.Version?>
|
||||
<?error Undefined Version variable?>
|
||||
<?endif?>
|
||||
<?ifndef var.Path?>
|
||||
<?error Undefined Path variable?>
|
||||
<?endif?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="35e5e858-9372-4449-bf73-1cd6f7267128"
|
||||
UpgradeCode="23f90fdd-9328-47ea-ab52-5380855a4b12"
|
||||
Name="cloudflared"
|
||||
Version="$(var.Version)"
|
||||
Manufacturer="cloudflare"
|
||||
Language="1033">
|
||||
|
||||
<Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package" InstallScope="perMachine"/>
|
||||
|
||||
<Media Id="1" Cabinet="product.cab" EmbedCab="yes"/>
|
||||
|
||||
<Upgrade Id="23f90fdd-9328-47ea-ab52-5380855a4b12">
|
||||
<UpgradeVersion Minimum="$(var.Version)" OnlyDetect="yes" Property="NEWERVERSIONDETECTED"/>
|
||||
<UpgradeVersion Minimum="2020.8.0" Maximum="$(var.Version)" IncludeMinimum="yes" IncludeMaximum="no"
|
||||
Property="OLDERVERSIONBEINGUPGRADED"/>
|
||||
</Upgrade>
|
||||
<Condition Message="A newer version of this software is already installed.">NOT NEWERVERSIONDETECTED</Condition>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<!--This specifies where the cloudflared.exe is moved to in the windows Operation System-->
|
||||
<Directory Id="$(var.Program_Files)">
|
||||
<Directory Id="INSTALLDIR" Name="cloudflared">
|
||||
<Component Id="ApplicationFiles" Guid="35e5e858-9372-4449-bf73-1cd6f7267128">
|
||||
<File Id="ApplicationFile0" Source="$(var.Path)"/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<Component Id="ENVS" Guid="6bb74449-d10d-4f4a-933e-6fc9fa006eae">
|
||||
<!--Set the cloudflared bin location to the Path Environment Variable-->
|
||||
<Environment Id="ENV0"
|
||||
Name="PATH"
|
||||
Value="[INSTALLDIR]."
|
||||
Permanent="no"
|
||||
Part="last"
|
||||
Action="create"
|
||||
System="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentRef Id="ENVS"/>
|
||||
<ComponentRef Id='ApplicationFiles' />
|
||||
</Feature>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -143,11 +143,13 @@ func Commands() []*cli.Command {
|
||||
Name: sshTokenIDFlag,
|
||||
Aliases: []string{"id"},
|
||||
Usage: "specify an Access service token ID you wish to use.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_ID"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshTokenSecretFlag,
|
||||
Aliases: []string{"secret"},
|
||||
Usage: "specify an Access service token secret you wish to use.",
|
||||
EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logger.LogSSHDirectoryFlag,
|
||||
|
||||
@@ -11,7 +11,7 @@ func RemovedCommand(name string) *cli.Command {
|
||||
Name: name,
|
||||
Action: func(context *cli.Context) error {
|
||||
return cli.Exit(
|
||||
fmt.Sprintf("%s command is no longer supported by cloudflared. Consult Argo Tunnel documentation for possible alternative solutions.", name),
|
||||
fmt.Sprintf("%s command is no longer supported by cloudflared. Consult Cloudflare Tunnel documentation for possible alternative solutions.", name),
|
||||
-1,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -19,11 +19,11 @@ import (
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel system service",
|
||||
Usage: "Manages the Cloudflare Tunnel system service",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a system service",
|
||||
Usage: "Install Cloudflare Tunnel as a system service",
|
||||
Action: cliutil.ConfiguredAction(installLinuxService),
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
@@ -34,7 +34,7 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Usage: "Uninstall the Cloudflare Tunnel service",
|
||||
Action: cliutil.ConfiguredAction(uninstallLinuxService),
|
||||
},
|
||||
},
|
||||
@@ -55,7 +55,7 @@ var systemdTemplates = []ServiceTemplate{
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared.service",
|
||||
Content: `[Unit]
|
||||
Description=Argo Tunnel
|
||||
Description=Cloudflare Tunnel
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@@ -72,7 +72,7 @@ WantedBy=multi-user.target
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.service",
|
||||
Content: `[Unit]
|
||||
Description=Update Argo Tunnel
|
||||
Description=Update Cloudflare Tunnel
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@@ -82,7 +82,7 @@ ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then s
|
||||
{
|
||||
Path: "/etc/systemd/system/cloudflared-update.timer",
|
||||
Content: `[Unit]
|
||||
Description=Update Argo Tunnel
|
||||
Description=Update Cloudflare Tunnel
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
@@ -99,7 +99,7 @@ var sysvTemplate = ServiceTemplate{
|
||||
Content: `#!/bin/sh
|
||||
# For RedHat and cousins:
|
||||
# chkconfig: 2345 99 01
|
||||
# description: Argo Tunnel agent
|
||||
# description: Cloudflare Tunnel agent
|
||||
# processname: {{.Path}}
|
||||
### BEGIN INIT INFO
|
||||
# Provides: {{.Path}}
|
||||
@@ -107,8 +107,8 @@ var sysvTemplate = ServiceTemplate{
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Argo Tunnel
|
||||
# Description: Argo Tunnel agent
|
||||
# Short-Description: Cloudflare Tunnel
|
||||
# Description: Cloudflare Tunnel agent
|
||||
### END INIT INFO
|
||||
name=$(basename $(readlink -f $0))
|
||||
cmd="{{.Path}} --config /etc/cloudflared/config.yml --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
|
||||
@@ -20,16 +20,16 @@ const (
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel launch agent",
|
||||
Usage: "Manages the Cloudflare Tunnel launch agent",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as an user launch agent",
|
||||
Usage: "Install Cloudflare Tunnel as an user launch agent",
|
||||
Action: cliutil.ConfiguredAction(installLaunchd),
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel launch agent",
|
||||
Usage: "Uninstall the Cloudflare Tunnel launch agent",
|
||||
Action: cliutil.ConfiguredAction(uninstallLaunchd),
|
||||
},
|
||||
},
|
||||
@@ -110,13 +110,13 @@ func installLaunchd(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
log.Info().Msg("Installing Argo Tunnel client as a system launch daemon. " +
|
||||
"Argo Tunnel client will run at boot")
|
||||
log.Info().Msg("Installing Cloudflare Tunnel client as a system launch daemon. " +
|
||||
"Cloudflare Tunnel client will run at boot")
|
||||
} else {
|
||||
log.Info().Msg("Installing Argo Tunnel client as an user launch agent. " +
|
||||
"Note that Argo Tunnel client will only run when the user is logged in. " +
|
||||
"If you want to run Argo Tunnel client at boot, install with root permission. " +
|
||||
"For more information, visit https://developers.cloudflare.com/argo-tunnel/reference/service/")
|
||||
log.Info().Msg("Installing Cloudflare Tunnel client as an user launch agent. " +
|
||||
"Note that Cloudflare Tunnel client will only run when the user is logged in. " +
|
||||
"If you want to run Cloudflare Tunnel client at boot, install with root permission. " +
|
||||
"For more information, visit https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service")
|
||||
}
|
||||
etPath, err := os.Executable()
|
||||
if err != nil {
|
||||
@@ -159,9 +159,9 @@ func uninstallLaunchd(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if isRootUser() {
|
||||
log.Info().Msg("Uninstalling Argo Tunnel as a system launch daemon")
|
||||
log.Info().Msg("Uninstalling Cloudflare Tunnel as a system launch daemon")
|
||||
} else {
|
||||
log.Info().Msg("Uninstalling Argo Tunnel as an user launch agent")
|
||||
log.Info().Msg("Uninstalling Cloudflare Tunnel as an user launch agent")
|
||||
}
|
||||
installPath, err := installPath()
|
||||
if err != nil {
|
||||
|
||||
@@ -67,7 +67,7 @@ func main() {
|
||||
app.Copyright = fmt.Sprintf(
|
||||
`(c) %d Cloudflare Inc.
|
||||
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
|
||||
the terms of the Cloudflare License (https://developers.cloudflare.com/argo-tunnel/license/),
|
||||
the terms of the Cloudflare License (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/license),
|
||||
Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`,
|
||||
time.Now().Year(),
|
||||
)
|
||||
@@ -76,7 +76,7 @@ func main() {
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.
|
||||
|
||||
See https://developers.cloudflare.com/argo-tunnel/ for more in-depth documentation.`
|
||||
See https://developers.cloudflare.com/cloudflare-one/connections/connect-apps for more in-depth documentation.`
|
||||
app.Flags = flags()
|
||||
app.Action = action(graceShutdownC)
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
@@ -73,7 +73,7 @@ func Run(c *cli.Context) error {
|
||||
log.Fatal().Err(err).Msg("Failed to open the metrics listener")
|
||||
}
|
||||
|
||||
go metrics.ServeMetrics(metricsListener, nil, nil, log)
|
||||
go metrics.ServeMetrics(metricsListener, nil, nil, "", log)
|
||||
|
||||
listener, err := tunneldns.CreateListener(
|
||||
c.String("address"),
|
||||
|
||||
@@ -87,6 +87,10 @@ const (
|
||||
var (
|
||||
graceShutdownC chan struct{}
|
||||
version string
|
||||
|
||||
routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+
|
||||
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
|
||||
"any existing DNS records for this hostname.", overwriteDNSFlag)
|
||||
)
|
||||
|
||||
func Flags() []cli.Flag {
|
||||
@@ -122,14 +126,14 @@ func buildTunnelCommand(subcommands []*cli.Command) *cli.Command {
|
||||
Name: "tunnel",
|
||||
Action: cliutil.ConfiguredAction(TunnelCommand),
|
||||
Category: "Tunnel",
|
||||
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
|
||||
Usage: "Make a locally-running web service accessible over the internet using Cloudflare Tunnel.",
|
||||
ArgsUsage: " ",
|
||||
Description: `Argo Tunnel asks you to specify a hostname on a Cloudflare-powered
|
||||
Description: `Cloudflare Tunnel asks you to specify a hostname on a Cloudflare-powered
|
||||
domain you control and a local address. Traffic from that hostname is routed
|
||||
(optionally via a Cloudflare Load Balancer) to this machine and appears on the
|
||||
specified port where it can be served.
|
||||
|
||||
This feature requires your Cloudflare account be subscribed to the Argo Smart Routing feature.
|
||||
This feature requires your Cloudflare account be subscribed to the Cloudflare Smart Routing feature.
|
||||
|
||||
To use, begin by calling login to download a certificate:
|
||||
|
||||
@@ -159,6 +163,12 @@ func TunnelCommand(c *cli.Context) error {
|
||||
return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
|
||||
}
|
||||
|
||||
// Unauthenticated named tunnel on <random>.<quick-tunnels-service>.com
|
||||
// For now, default to legacy setup unless quick-service is specified
|
||||
if !dnsProxyStandAlone(c, nil) && c.String("hostname") == "" && c.String("quick-service") != "" {
|
||||
return RunQuickTunnel(sc)
|
||||
}
|
||||
|
||||
// Start a classic tunnel
|
||||
return runClassicTunnel(sc)
|
||||
}
|
||||
@@ -181,7 +191,7 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri
|
||||
|
||||
if r, ok := routeFromFlag(sc.c); ok {
|
||||
if res, err := sc.route(tunnel.ID, r); err != nil {
|
||||
sc.log.Err(err).Str("route", r.String()).Msg("failed to provision routing, please create it manually via Cloudflare dashboard or UI; most likely you already have a conflicting record there")
|
||||
sc.log.Err(err).Str("route", r.String()).Msg(routeFailMsg)
|
||||
} else {
|
||||
sc.log.Info().Msg(res.SuccessSummary())
|
||||
}
|
||||
@@ -199,12 +209,12 @@ func runClassicTunnel(sc *subcommandContext) error {
|
||||
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
|
||||
}
|
||||
|
||||
func routeFromFlag(c *cli.Context) (tunnelstore.Route, bool) {
|
||||
func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) {
|
||||
if hostname := c.String("hostname"); hostname != "" {
|
||||
if lbPool := c.String("lb-pool"); lbPool != "" {
|
||||
return tunnelstore.NewLBRoute(hostname, lbPool), true
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(hostname), true
|
||||
return tunnelstore.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -314,6 +324,15 @@ func StartServer(
|
||||
|
||||
observer := connection.NewObserver(log, logTransport, isUIEnabled)
|
||||
|
||||
// Send Quick Tunnel URL to UI if applicable
|
||||
var quickTunnelURL string
|
||||
if namedTunnel != nil {
|
||||
quickTunnelURL = namedTunnel.QuickTunnelUrl
|
||||
}
|
||||
if quickTunnelURL != "" {
|
||||
observer.SendURL(quickTunnelURL)
|
||||
}
|
||||
|
||||
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, buildInfo, version, log, logTransport, observer, namedTunnel)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't start tunnel")
|
||||
@@ -331,7 +350,7 @@ func StartServer(
|
||||
defer wg.Done()
|
||||
readinessServer := metrics.NewReadyServer(log)
|
||||
observer.RegisterSink(readinessServer)
|
||||
errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, log)
|
||||
errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, log)
|
||||
}()
|
||||
|
||||
if err := ingressRules.StartOrigins(&wg, log, ctx.Done(), errC); err != nil {
|
||||
@@ -365,7 +384,11 @@ func StartServer(
|
||||
observer.RegisterSink(app)
|
||||
}
|
||||
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, c.Duration("grace-period"), log)
|
||||
gracePeriod, err := gracePeriod(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return waitToShutdown(&wg, cancel, errC, graceShutdownC, gracePeriod, log)
|
||||
}
|
||||
|
||||
func waitToShutdown(wg *sync.WaitGroup,
|
||||
@@ -465,7 +488,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
credentialsFileFlag,
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "is-autoupdated",
|
||||
Usage: "Signal the new process that Argo Tunnel connector has been autoupdated",
|
||||
Usage: "Signal the new process that Cloudflare Tunnel connector has been autoupdated",
|
||||
Value: false,
|
||||
Hidden: true,
|
||||
}),
|
||||
@@ -475,6 +498,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
EnvVars: []string{"TUNNEL_EDGE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "region",
|
||||
Usage: "Cloudflare Edge region to connect to. Omit or set to empty to connect to the global region.",
|
||||
EnvVars: []string{"TUNNEL_REGION"},
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: tlsconfig.CaCertFlag,
|
||||
Usage: "Certificate Authority authenticating connections with Cloudflare's edge network.",
|
||||
@@ -612,7 +640,20 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "quick-service",
|
||||
Usage: "URL for a service which manages unauthenticated 'quick' tunnels.",
|
||||
Value: "https://api.trycloudflare.com",
|
||||
Hidden: true,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: "max-fetch-size",
|
||||
Usage: `The maximum number of results that cloudflared can fetch from Cloudflare API for any listing operations needed`,
|
||||
EnvVars: []string{"TUNNEL_MAX_FETCH_SIZE"},
|
||||
Hidden: true,
|
||||
}),
|
||||
selectProtocolFlag,
|
||||
overwriteDNSFlag,
|
||||
}...)
|
||||
|
||||
return flags
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
@@ -161,7 +162,6 @@ func prepareTunnelConfig(
|
||||
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(log)
|
||||
@@ -178,19 +178,6 @@ func prepareTunnelConfig(
|
||||
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
originCertPath := c.String("origincert")
|
||||
originCertLog := log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCert, err = getOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelConfig
|
||||
@@ -217,6 +204,17 @@ func prepareTunnelConfig(
|
||||
return nil, ingress.Ingress{}, ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
} else {
|
||||
|
||||
originCertPath := c.String("origincert")
|
||||
originCertLog := log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
Logger()
|
||||
|
||||
originCert, err := getOriginCert(originCertPath, &originCertLog)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Error getting origin cert")
|
||||
}
|
||||
|
||||
classicTunnel = &connection.ClassicTunnelConfig{
|
||||
Hostname: hostname,
|
||||
OriginCert: originCert,
|
||||
@@ -240,7 +238,7 @@ func prepareTunnelConfig(
|
||||
log.Info().Msgf("Warp-routing is enabled")
|
||||
}
|
||||
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.HTTP2Percentage, origin.ResolveTTL, log)
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.ProtocolPercentage, origin.ResolveTTL, log)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
@@ -248,17 +246,28 @@ func prepareTunnelConfig(
|
||||
|
||||
edgeTLSConfigs := make(map[connection.Protocol]*tls.Config, len(connection.ProtocolList))
|
||||
for _, p := range connection.ProtocolList {
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, p.ServerName())
|
||||
tlsSettings := p.TLSSettings()
|
||||
if tlsSettings == nil {
|
||||
return nil, ingress.Ingress{}, fmt.Errorf("%s has unknown TLS settings", p)
|
||||
}
|
||||
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "unable to create TLS config to connect with edge")
|
||||
}
|
||||
if len(tlsSettings.NextProtos) > 0 {
|
||||
edgeTLSConfig.NextProtos = tlsSettings.NextProtos
|
||||
}
|
||||
edgeTLSConfigs[p] = edgeTLSConfig
|
||||
}
|
||||
|
||||
originProxy := origin.NewOriginProxy(ingressRules, warpRoutingService, tags, log)
|
||||
gracePeriod, err := gracePeriod(c)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
connectionConfig := &connection.Config{
|
||||
OriginProxy: originProxy,
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
GracePeriod: gracePeriod,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
}
|
||||
muxerConfig := &connection.MuxerConfig{
|
||||
@@ -275,10 +284,10 @@ func prepareTunnelConfig(
|
||||
OSArch: buildInfo.OSArch(),
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
@@ -296,6 +305,14 @@ func prepareTunnelConfig(
|
||||
}, ingressRules, nil
|
||||
}
|
||||
|
||||
func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
period := c.Duration("grace-period")
|
||||
if period > connection.MaxGracePeriod {
|
||||
return time.Duration(0), fmt.Errorf("grace-period must be equal or less than %v", connection.MaxGracePeriod)
|
||||
}
|
||||
return period, nil
|
||||
}
|
||||
|
||||
func isWarpRoutingEnabled(warpConfig config.WarpRoutingConfig, isNamedTunnel bool) bool {
|
||||
return warpConfig.Enabled && isNamedTunnel
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
const (
|
||||
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
|
||||
callbackStoreURL = "https://login.argotunnel.com/"
|
||||
callbackStoreURL = "https://login.cloudflareaccess.org/"
|
||||
)
|
||||
|
||||
func buildLoginSubcommand(hidden bool) *cli.Command {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
const httpTimeout = 15 * time.Second
|
||||
|
||||
const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to" +
|
||||
" experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you " +
|
||||
"intend to use Tunnels in production you should use a pre-created named tunnel by following: " +
|
||||
"https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"
|
||||
|
||||
// RunQuickTunnel requests a tunnel from the specified service.
|
||||
// We use this to power quick tunnels on trycloudflare.com, but the
|
||||
// service is open-source and could be used by anyone.
|
||||
func RunQuickTunnel(sc *subcommandContext) error {
|
||||
sc.log.Info().Msg(disclaimer)
|
||||
sc.log.Info().Msg("Requesting new quick Tunnel on trycloudflare.com...")
|
||||
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSHandshakeTimeout: httpTimeout,
|
||||
ResponseHeaderTimeout: httpTimeout,
|
||||
},
|
||||
Timeout: httpTimeout,
|
||||
}
|
||||
|
||||
resp, err := client.Post(fmt.Sprintf("%s/tunnel", sc.c.String("quick-service")), "application/json", nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var data QuickTunnelResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal quick Tunnel")
|
||||
}
|
||||
|
||||
tunnelID, err := uuid.Parse(data.Result.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse quick Tunnel ID")
|
||||
}
|
||||
|
||||
credentials := connection.Credentials{
|
||||
AccountTag: data.Result.AccountTag,
|
||||
TunnelSecret: data.Result.Secret,
|
||||
TunnelID: tunnelID,
|
||||
TunnelName: data.Result.Name,
|
||||
}
|
||||
|
||||
url := data.Result.Hostname
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
for _, line := range AsciiBox([]string{
|
||||
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
||||
url,
|
||||
}, 2) {
|
||||
sc.log.Info().Msg(line)
|
||||
}
|
||||
|
||||
if !sc.c.IsSet("protocol") {
|
||||
sc.c.Set("protocol", "quic")
|
||||
}
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
version,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
type QuickTunnelResponse struct {
|
||||
Success bool
|
||||
Result QuickTunnel
|
||||
Errors []QuickTunnelError
|
||||
}
|
||||
|
||||
type QuickTunnelError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
type QuickTunnel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname"`
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func AsciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath 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")
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
|
||||
}
|
||||
|
||||
tunnelSecret, err := generateTunnelSecret()
|
||||
@@ -230,7 +230,7 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
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)
|
||||
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", id)
|
||||
}
|
||||
|
||||
// Check if tunnel DeletedAt field has already been set
|
||||
@@ -261,8 +261,16 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
// and add the TunnelID into any old credentials (generated before TUN-3581 added the `TunnelID`
|
||||
// field to credentials files)
|
||||
func (sc *subcommandContext) findCredentials(tunnelID uuid.UUID) (connection.Credentials, error) {
|
||||
credFinder := sc.credentialFinder(tunnelID)
|
||||
credentials, err := sc.readTunnelCredentials(credFinder)
|
||||
var credentials connection.Credentials
|
||||
var err error
|
||||
if credentialsContents := sc.c.String(CredContentsFlag); credentialsContents != "" {
|
||||
if err = json.Unmarshal([]byte(credentialsContents), &credentials); err != nil {
|
||||
err = errInvalidJSONCredential{path: "TUNNEL_CRED_CONTENTS", err: err}
|
||||
}
|
||||
} else {
|
||||
credFinder := sc.credentialFinder(tunnelID)
|
||||
credentials, err = sc.readTunnelCredentials(credFinder)
|
||||
}
|
||||
// This line ensures backwards compatibility with credentials files generated before
|
||||
// TUN-3581. Those old credentials files don't have a TunnelID field, so we enrich the struct
|
||||
// with the ID, which we have already resolved from the user input.
|
||||
@@ -365,52 +373,42 @@ func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
|
||||
}
|
||||
|
||||
// findIDs is just like mapping `findID` over a slice, but it only uses
|
||||
// one Tunnelstore API call.
|
||||
// one Tunnelstore API call per non-UUID input provided.
|
||||
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||
uuids, names := splitUuids(inputs)
|
||||
|
||||
// Shortcut without Tunnelstore call if we find that all inputs are already UUIDs.
|
||||
uuids, err := convertNamesToUuids(inputs, make(map[string]uuid.UUID))
|
||||
if err == nil {
|
||||
return uuids, nil
|
||||
for _, name := range names {
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tunnels) != 1 {
|
||||
return nil, fmt.Errorf("there should only be 1 non-deleted Tunnel named %s", name)
|
||||
}
|
||||
|
||||
uuids = append(uuids, tunnels[0].ID)
|
||||
}
|
||||
|
||||
// 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)
|
||||
return uuids, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
func splitUuids(inputs []string) ([]uuid.UUID, []string) {
|
||||
uuids := make([]uuid.UUID, 0)
|
||||
names := make([]string, 0)
|
||||
|
||||
return convertNamesToUuids(inputs, nameToID)
|
||||
}
|
||||
|
||||
func convertNamesToUuids(inputs []string, nameToID map[string]uuid.UUID) ([]uuid.UUID, error) {
|
||||
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
|
||||
for _, input := range inputs {
|
||||
id, err := uuid.Parse(input)
|
||||
if err != nil {
|
||||
names = append(names, input)
|
||||
} else {
|
||||
badInputs = append(badInputs, input)
|
||||
uuids = append(uuids, id)
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
return uuids, names
|
||||
}
|
||||
|
||||
@@ -17,79 +17,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockFileSystem struct {
|
||||
rf func(string) ([]byte, error)
|
||||
vfp func(string) bool
|
||||
@@ -191,6 +118,51 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
TunnelName: name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TUNNEL_CRED_CONTENTS given contains old credentials contents",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredContentsFlag, "", "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s"}`, accountTag, secretB64))
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TUNNEL_CRED_CONTENTS given contains new credentials contents",
|
||||
fields: fields{
|
||||
log: &log,
|
||||
fs: fs,
|
||||
c: func() *cli.Context {
|
||||
flagSet := flag.NewFlagSet("test0", flag.PanicOnError)
|
||||
flagSet.String(CredContentsFlag, "", "")
|
||||
c := cli.NewContext(cli.NewApp(), flagSet, nil)
|
||||
_ = c.Set(CredContentsFlag, fmt.Sprintf(`{"AccountTag":"%s","TunnelSecret":"%s","TunnelID":"%s","TunnelName":"%s"}`, accountTag, secretB64, tunnelID, name))
|
||||
return c
|
||||
}(),
|
||||
},
|
||||
args: args{
|
||||
tunnelID: tunnelID,
|
||||
},
|
||||
want: connection.Credentials{
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
TunnelName: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -29,10 +29,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
|
||||
connsSortByOptions = "id, startedAt, numConnections, version"
|
||||
CredFileFlagAlias = "cred-file"
|
||||
CredFileFlag = "credentials-file"
|
||||
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
|
||||
connsSortByOptions = "id, startedAt, numConnections, version"
|
||||
CredFileFlagAlias = "cred-file"
|
||||
CredFileFlag = "credentials-file"
|
||||
CredContentsFlag = "credentials-contents"
|
||||
overwriteDNSFlagName = "overwrite-dns"
|
||||
|
||||
LogFieldTunnelID = "tunnelID"
|
||||
)
|
||||
@@ -48,6 +50,16 @@ var (
|
||||
Aliases: []string{"n"},
|
||||
Usage: "List tunnels with the given `NAME`",
|
||||
}
|
||||
listNamePrefixFlag = &cli.StringFlag{
|
||||
Name: "name-prefix",
|
||||
Aliases: []string{"np"},
|
||||
Usage: "List tunnels that start with the give `NAME` prefix",
|
||||
}
|
||||
listExcludeNamePrefixFlag = &cli.StringFlag{
|
||||
Name: "exclude-name-prefix",
|
||||
Aliases: []string{"enp"},
|
||||
Usage: "List tunnels whose `NAME` does not start with the given prefix",
|
||||
}
|
||||
listExistedAtFlag = &cli.TimestampFlag{
|
||||
Name: "when",
|
||||
Aliases: []string{"w"},
|
||||
@@ -100,8 +112,13 @@ var (
|
||||
Usage: "Filepath at which to read/write the tunnel credentials",
|
||||
EnvVars: []string{"TUNNEL_CRED_FILE"},
|
||||
}
|
||||
credentialsFileFlag = altsrc.NewStringFlag(credentialsFileFlagCLIOnly)
|
||||
forceDeleteFlag = &cli.BoolFlag{
|
||||
credentialsFileFlag = altsrc.NewStringFlag(credentialsFileFlagCLIOnly)
|
||||
credentialsContentsFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: CredContentsFlag,
|
||||
Usage: "Contents of the tunnel credentials JSON file to use. When provided along with credentials-file, this will take precedence.",
|
||||
EnvVars: []string{"TUNNEL_CRED_CONTENTS"},
|
||||
})
|
||||
forceDeleteFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Cleans up any stale connections before the tunnel is deleted. cloudflared will not " +
|
||||
@@ -110,7 +127,7 @@ var (
|
||||
}
|
||||
selectProtocolFlag = altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: "protocol",
|
||||
Value: "h2mux",
|
||||
Value: "auto",
|
||||
Aliases: []string{"p"},
|
||||
Usage: fmt.Sprintf("Protocol implementation to connect with Cloudflare's edge network. %s", connection.AvailableProtocolFlagMessage),
|
||||
EnvVars: []string{"TUNNEL_TRANSPORT_PROTOCOL"},
|
||||
@@ -133,6 +150,12 @@ var (
|
||||
Usage: `Constraints the cleanup to stop the connections of a single Connector (by its ID). You can find the various Connectors (and their IDs) currently connected to your tunnel via 'cloudflared tunnel info <name>'.`,
|
||||
EnvVars: []string{"TUNNEL_CLEANUP_CONNECTOR"},
|
||||
}
|
||||
overwriteDNSFlag = &cli.BoolFlag{
|
||||
Name: overwriteDNSFlagName,
|
||||
Aliases: []string{"f"},
|
||||
Usage: `Overwrites existing DNS records with this hostname`,
|
||||
EnvVars: []string{"TUNNEL_FORCE_PROVISIONING_DNS"},
|
||||
}
|
||||
)
|
||||
|
||||
func buildCreateCommand() *cli.Command {
|
||||
@@ -210,6 +233,8 @@ func buildListCommand() *cli.Command {
|
||||
outputFormatFlag,
|
||||
showDeletedFlag,
|
||||
listNameFlag,
|
||||
listNamePrefixFlag,
|
||||
listExcludeNamePrefixFlag,
|
||||
listExistedAtFlag,
|
||||
listIDFlag,
|
||||
showRecentlyDisconnected,
|
||||
@@ -236,6 +261,12 @@ func listCommand(c *cli.Context) error {
|
||||
if name := c.String("name"); name != "" {
|
||||
filter.ByName(name)
|
||||
}
|
||||
if namePrefix := c.String("name-prefix"); namePrefix != "" {
|
||||
filter.ByNamePrefix(namePrefix)
|
||||
}
|
||||
if excludePrefix := c.String("exclude-name-prefix"); excludePrefix != "" {
|
||||
filter.ExcludeNameWithPrefix(excludePrefix)
|
||||
}
|
||||
if existedAt := c.Timestamp("time"); existedAt != nil {
|
||||
filter.ByExistedAt(*existedAt)
|
||||
}
|
||||
@@ -246,6 +277,9 @@ func listCommand(c *cli.Context) error {
|
||||
}
|
||||
filter.ByTunnelID(tunnelID)
|
||||
}
|
||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||
filter.MaxFetchSize(uint(maxFetch))
|
||||
}
|
||||
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
@@ -554,6 +588,7 @@ func buildRunCommand() *cli.Command {
|
||||
flags := []cli.Flag{
|
||||
forceFlag,
|
||||
credentialsFileFlag,
|
||||
credentialsContentsFlag,
|
||||
selectProtocolFlag,
|
||||
featuresFlag,
|
||||
}
|
||||
@@ -648,7 +683,6 @@ func cleanupCommand(c *cli.Context) error {
|
||||
func buildRouteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "route",
|
||||
Action: cliutil.ConfiguredAction(routeCommand),
|
||||
Usage: "Define which traffic routed from Cloudflare edge to this tunnel: requests to a DNS hostname, to a Cloudflare Load Balancer, or traffic originating from Cloudflare WARP clients",
|
||||
UsageText: "cloudflared tunnel [tunnel command options] route [subcommand options] [dns TUNNEL HOSTNAME]|[lb TUNNEL HOSTNAME LB-POOL]|[ip NETWORK TUNNEL]",
|
||||
Description: `The route command defines how Cloudflare will proxy requests to this tunnel.
|
||||
@@ -668,15 +702,30 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||
`,
|
||||
CustomHelpTemplate: commandHelpTemplate(),
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "dns",
|
||||
Action: cliutil.ConfiguredAction(routeDnsCommand),
|
||||
Usage: "Route a hostname by creating a DNS CNAME record to a tunnel",
|
||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]",
|
||||
Description: `Creates a DNS CNAME record hostname that points to the tunnel.`,
|
||||
Flags: []cli.Flag{overwriteDNSFlag},
|
||||
},
|
||||
{
|
||||
Name: "lb",
|
||||
Action: cliutil.ConfiguredAction(routeLbCommand),
|
||||
Usage: "Use this tunnel as a load balancer origin, creating pool and load balancer if necessary",
|
||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME] [LB-POOL]",
|
||||
Description: `Creates Load Balancer with an origin pool that points to the tunnel.`,
|
||||
},
|
||||
buildRouteIPSubcommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route, error) {
|
||||
const (
|
||||
userHostnameIndex = 2
|
||||
expectedNArgs = 3
|
||||
userHostnameIndex = 1
|
||||
expectedNArgs = 2
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
@@ -687,14 +736,14 @@ func dnsRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
} else if !validateHostname(userHostname, true) {
|
||||
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(userHostname), nil
|
||||
return tunnelstore.NewDNSRoute(userHostname, overwriteExisting), nil
|
||||
}
|
||||
|
||||
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
const (
|
||||
lbNameIndex = 2
|
||||
lbPoolIndex = 3
|
||||
expectedNArgs = 4
|
||||
lbNameIndex = 1
|
||||
lbPoolIndex = 2
|
||||
expectedNArgs = 3
|
||||
)
|
||||
if c.NArg() != expectedNArgs {
|
||||
return nil, cliutil.UsageError("Expected %d arguments, got %d", expectedNArgs, c.NArg())
|
||||
@@ -736,41 +785,39 @@ func validateHostname(s string, allowWildcardSubdomain bool) bool {
|
||||
return err == nil && validateName(puny, allowWildcardSubdomain)
|
||||
}
|
||||
|
||||
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`)
|
||||
func routeDnsCommand(c *cli.Context) error {
|
||||
if c.NArg() != 2 {
|
||||
return cliutil.UsageError(`This command expects the format "cloudflared tunnel route dns <tunnel name/id> <hostname>"`)
|
||||
}
|
||||
return routeCommand(c, "dns")
|
||||
}
|
||||
|
||||
func routeLbCommand(c *cli.Context) error {
|
||||
if c.NArg() != 3 {
|
||||
return cliutil.UsageError(`This command expects the format "cloudflared tunnel route lb <tunnel name/id> <hostname> <load balancer pool>"`)
|
||||
}
|
||||
return routeCommand(c, "lb")
|
||||
}
|
||||
|
||||
func routeCommand(c *cli.Context, routeType string) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const tunnelIDIndex = 1
|
||||
|
||||
routeType := c.Args().First()
|
||||
tunnelID, err := sc.findID(c.Args().Get(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName))
|
||||
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)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := sc.route(tunnelID, route)
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
DefaultCheckUpdateFreq = time.Hour * 24
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
|
||||
isManagedInstallFile = ".installedFromPackageManager"
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
|
||||
const (
|
||||
windowsServiceName = "Cloudflared"
|
||||
windowsServiceDescription = "Argo Tunnel agent"
|
||||
windowsServiceUrl = "https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
windowsServiceDescription = "Cloudflare Tunnel agent"
|
||||
windowsServiceUrl = "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service#windows"
|
||||
|
||||
recoverActionDelay = time.Second * 20
|
||||
failureCountResetPeriod = time.Hour * 24
|
||||
@@ -45,16 +45,16 @@ const (
|
||||
func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
app.Commands = append(app.Commands, &cli.Command{
|
||||
Name: "service",
|
||||
Usage: "Manages the Argo Tunnel Windows service",
|
||||
Usage: "Manages the Cloudflare Tunnel Windows service",
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a Windows service",
|
||||
Usage: "Install Cloudflare Tunnel as a Windows service",
|
||||
Action: cliutil.ConfiguredAction(installWindowsService),
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Usage: "Uninstall the Cloudflare Tunnel service",
|
||||
Action: cliutil.ConfiguredAction(uninstallWindowsService),
|
||||
},
|
||||
},
|
||||
@@ -176,7 +176,7 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
|
||||
func installWindowsService(c *cli.Context) error {
|
||||
zeroLogger := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
zeroLogger.Info().Msg("Installing Argo Tunnel Windows service")
|
||||
zeroLogger.Info().Msg("Installing Cloudflare Tunnel Windows service")
|
||||
exepath, err := os.Executable()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot find path name that start the process")
|
||||
@@ -198,7 +198,7 @@ func installWindowsService(c *cli.Context) error {
|
||||
return errors.Wrap(err, "Cannot install service")
|
||||
}
|
||||
defer s.Close()
|
||||
log.Info().Msg("Argo Tunnel agent service is installed")
|
||||
log.Info().Msg("Cloudflare Tunnel agent service is installed")
|
||||
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
@@ -217,7 +217,7 @@ func uninstallWindowsService(c *cli.Context) error {
|
||||
With().
|
||||
Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
|
||||
|
||||
log.Info().Msg("Uninstalling Argo Tunnel Windows Service")
|
||||
log.Info().Msg("Uninstalling Cloudflare Tunnel Windows Service")
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot establish a connection to the service control manager")
|
||||
@@ -232,7 +232,7 @@ func uninstallWindowsService(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot delete service")
|
||||
}
|
||||
log.Info().Msg("Argo Tunnel agent service is uninstalled")
|
||||
log.Info().Msg("Cloudflare Tunnel agent service is uninstalled")
|
||||
err = eventlog.Remove(windowsServiceName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot remove event logger")
|
||||
|
||||
@@ -3,7 +3,7 @@ import copy
|
||||
|
||||
from dataclasses import dataclass, InitVar
|
||||
|
||||
from constants import METRICS_PORT
|
||||
from constants import METRICS_PORT, PROXY_DNS_PORT
|
||||
|
||||
# frozen=True raises exception when assigning to fields. This emulates immutability
|
||||
|
||||
@@ -93,3 +93,12 @@ class ClassicTunnelConfig(ClassicTunnelBaseConfig):
|
||||
|
||||
def get_url(self):
|
||||
return "https://" + self.hostname
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProxyDnsConfig(BaseConfig):
|
||||
full_config = {
|
||||
"port": PROXY_DNS_PORT,
|
||||
"no-autoupdate": True,
|
||||
}
|
||||
|
||||
|
||||
+29
-14
@@ -1,14 +1,21 @@
|
||||
import os
|
||||
from enum import Enum, auto
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from time import sleep
|
||||
|
||||
from config import NamedTunnelConfig, ClassicTunnelConfig
|
||||
from constants import BACKOFF_SECS
|
||||
from config import NamedTunnelConfig, ClassicTunnelConfig, ProxyDnsConfig
|
||||
from constants import BACKOFF_SECS, PROXY_DNS_PORT
|
||||
from util import LOGGER
|
||||
|
||||
|
||||
class CfdModes(Enum):
|
||||
NAMED = auto()
|
||||
CLASSIC = auto()
|
||||
PROXY_DNS = auto()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def component_tests_config():
|
||||
config_file = os.getenv("COMPONENT_TESTS_CONFIG")
|
||||
@@ -19,22 +26,30 @@ def component_tests_config():
|
||||
config = yaml.safe_load(stream)
|
||||
LOGGER.info(f"component tests base config {config}")
|
||||
|
||||
def _component_tests_config(additional_config={}, named_tunnel=True):
|
||||
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True):
|
||||
if run_proxy_dns:
|
||||
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
|
||||
# So we run all tests with it.
|
||||
additional_config["proxy-dns"] = True
|
||||
additional_config["proxy-dns-port"] = PROXY_DNS_PORT
|
||||
else:
|
||||
additional_config.pop("proxy-dns", None)
|
||||
additional_config.pop("proxy-dns-port", None)
|
||||
|
||||
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running
|
||||
additional_config["proxy-dns"] = True
|
||||
additional_config["proxy-dns-port"] = 9053
|
||||
|
||||
if named_tunnel:
|
||||
if cfd_mode is CfdModes.NAMED:
|
||||
return NamedTunnelConfig(additional_config=additional_config,
|
||||
cloudflared_binary=config['cloudflared_binary'],
|
||||
tunnel=config['tunnel'],
|
||||
credentials_file=config['credentials_file'],
|
||||
ingress=config['ingress'])
|
||||
|
||||
return ClassicTunnelConfig(
|
||||
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'],
|
||||
hostname=config['classic_hostname'], origincert=config['origincert'])
|
||||
elif cfd_mode is CfdModes.CLASSIC:
|
||||
return ClassicTunnelConfig(
|
||||
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'],
|
||||
hostname=config['classic_hostname'], origincert=config['origincert'])
|
||||
elif cfd_mode is CfdModes.PROXY_DNS:
|
||||
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
|
||||
else:
|
||||
raise Exception(f"Unknown cloudflared mode {cfd_mode}")
|
||||
|
||||
return _component_tests_config
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
METRICS_PORT = 51000
|
||||
MAX_RETRIES = 5
|
||||
BACKOFF_SECS = 7
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
|
||||
@@ -7,7 +7,7 @@ from util import start_cloudflared, wait_tunnel_ready, send_requests
|
||||
# Rolling logger rotate log files after 1 MB
|
||||
rotate_after_size = 1000 * 1000
|
||||
default_log_file = "cloudflared.log"
|
||||
expect_message = "Starting Hello World server"
|
||||
expect_message = "Starting Hello"
|
||||
|
||||
|
||||
def assert_log_to_terminal(cloudflared):
|
||||
@@ -17,7 +17,7 @@ def assert_log_to_terminal(cloudflared):
|
||||
|
||||
def assert_log_in_file(file):
|
||||
with open(file, "r") as f:
|
||||
log = f.read(1850)
|
||||
log = f.read(2000)
|
||||
assert expect_message in log, f"{log} doesn't contain {expect_message}"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
import socket
|
||||
from time import sleep
|
||||
|
||||
import constants
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
# Sanity checks that test that we only run Proxy DNS and Tunnel when we really expect them to be there.
|
||||
class TestProxyDns:
|
||||
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_with_classic_tunnel(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=True)
|
||||
|
||||
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
|
||||
|
||||
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
|
||||
|
||||
def test_classic_tunnel_alone(self, tmp_path, component_tests_config):
|
||||
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=False)
|
||||
|
||||
|
||||
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
|
||||
expect_proxy_dns = run_proxy_dns
|
||||
expect_tunnel = False
|
||||
|
||||
if cfd_mode == CfdModes.NAMED:
|
||||
expect_tunnel = True
|
||||
pre_args = ["tunnel"]
|
||||
args = ["run"]
|
||||
elif cfd_mode == CfdModes.CLASSIC:
|
||||
expect_tunnel = True
|
||||
pre_args = []
|
||||
args = []
|
||||
elif cfd_mode == CfdModes.PROXY_DNS:
|
||||
expect_proxy_dns = True
|
||||
pre_args = []
|
||||
args = ["proxy-dns", "--port", str(constants.PROXY_DNS_PORT)]
|
||||
else:
|
||||
assert False, f"Unknown cfd_mode {cfd_mode}"
|
||||
|
||||
config = component_tests_config(cfd_mode=cfd_mode, run_proxy_dns=run_proxy_dns)
|
||||
with start_cloudflared(tmp_path, config, cfd_pre_args=pre_args, cfd_args=args, new_process=True, capture_output=False):
|
||||
if expect_tunnel:
|
||||
wait_tunnel_ready()
|
||||
else:
|
||||
check_tunnel_not_connected()
|
||||
verify_proxy_dns(expect_proxy_dns)
|
||||
|
||||
|
||||
def verify_proxy_dns(should_be_running):
|
||||
# Wait for the Proxy DNS listener to come up.
|
||||
sleep(constants.BACKOFF_SECS)
|
||||
had_failure = False
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(('localhost', constants.PROXY_DNS_PORT))
|
||||
sock.send(b"anything")
|
||||
except:
|
||||
if should_be_running:
|
||||
assert False, "Expected Proxy DNS to be running, but it was not."
|
||||
had_failure = True
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
if not should_be_running and not had_failure:
|
||||
assert False, "Proxy DNS should not have been running, but it was."
|
||||
@@ -1,20 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
import copy
|
||||
import platform
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
from flaky import flaky
|
||||
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
@flaky(max_runs=3, min_passes=1)
|
||||
class TestReconnect():
|
||||
class TestReconnect:
|
||||
default_ha_conns = 4
|
||||
default_reconnect_secs = 5
|
||||
default_reconnect_secs = 15
|
||||
extra_config = {
|
||||
"stdin-control": True,
|
||||
}
|
||||
|
||||
@pytest.mark.skipif(platform.system() == "Windows", reason=f"Currently buggy on Windows TUN-4584")
|
||||
def test_named_reconnect(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(self.extra_config)
|
||||
with start_cloudflared(tmp_path, config, new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
@@ -24,8 +28,7 @@ class TestReconnect():
|
||||
def test_classic_reconnect(self, tmp_path, component_tests_config):
|
||||
extra_config = copy.copy(self.extra_config)
|
||||
extra_config["hello-world"] = True
|
||||
config = component_tests_config(
|
||||
additional_config=extra_config, named_tunnel=False)
|
||||
config = component_tests_config(additional_config=extra_config, cfd_mode=CfdModes.CLASSIC)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
self.assert_reconnect(config, cloudflared, 1)
|
||||
|
||||
@@ -36,7 +39,7 @@ class TestReconnect():
|
||||
cloudflared.stdin.flush()
|
||||
|
||||
def assert_reconnect(self, config, cloudflared, repeat):
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
|
||||
for _ in range(repeat):
|
||||
for i in range(self.default_ha_conns):
|
||||
self.send_reconnect(cloudflared, self.default_reconnect_secs)
|
||||
@@ -44,11 +47,9 @@ class TestReconnect():
|
||||
if expect_connections > 0:
|
||||
# Don't check if tunnel returns 200 here because there is a race condition between wait_tunnel_ready
|
||||
# retrying to get 200 response and reconnecting
|
||||
wait_tunnel_ready(
|
||||
require_min_connections=expect_connections)
|
||||
wait_tunnel_ready(require_min_connections=expect_connections)
|
||||
else:
|
||||
check_tunnel_not_connected()
|
||||
|
||||
sleep(self.default_reconnect_secs + 10)
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(),
|
||||
require_min_connections=self.default_ha_conns)
|
||||
sleep(self.default_reconnect_secs * 2)
|
||||
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
|
||||
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
import test_logging
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready
|
||||
|
||||
|
||||
@@ -34,7 +35,7 @@ class TestServiceMode:
|
||||
"hello-world": True,
|
||||
"logfile": str(log_file),
|
||||
}
|
||||
config = component_tests_config(additional_config=additional_config, named_tunnel=False)
|
||||
config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC)
|
||||
|
||||
def assert_log_file():
|
||||
test_logging.assert_log_in_file(log_file)
|
||||
@@ -52,7 +53,7 @@ class TestServiceMode:
|
||||
"loglevel": "debug",
|
||||
"log-directory": str(log_dir),
|
||||
}
|
||||
config = component_tests_config(additional_config=additional_config, named_tunnel=False)
|
||||
config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC)
|
||||
|
||||
def assert_rotating_log():
|
||||
test_logging.assert_log_to_dir(config, log_dir)
|
||||
|
||||
@@ -34,7 +34,7 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
|
||||
cmd += ["sudo"]
|
||||
cmd += [config.cloudflared_binary]
|
||||
cmd += cfd_pre_args
|
||||
cmd += ["--config", config_path]
|
||||
cmd += ["--config", str(config_path)]
|
||||
cmd += cfd_args
|
||||
LOGGER.info(f"Run cmd {cmd} with config {config}")
|
||||
return cmd
|
||||
@@ -50,8 +50,7 @@ def run_cloudflared_background(cmd, allow_input, capture_output):
|
||||
finally:
|
||||
cfd.terminate()
|
||||
if capture_output:
|
||||
LOGGER.info(
|
||||
f"cloudflared log: {cfd.stderr.read()}")
|
||||
LOGGER.info(f"cloudflared log: {cfd.stderr.read()}")
|
||||
|
||||
|
||||
@retry(stop_max_attempt_number=MAX_RETRIES, wait_fixed=BACKOFF_SECS * 1000)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -11,9 +13,17 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const LogFieldConnIndex = "connIndex"
|
||||
const (
|
||||
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
|
||||
LogFieldConnIndex = "connIndex"
|
||||
MaxGracePeriod = time.Minute * 3
|
||||
MaxConcurrentStreams = math.MaxUint32
|
||||
)
|
||||
|
||||
var switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
|
||||
type Config struct {
|
||||
OriginProxy OriginProxy
|
||||
@@ -22,8 +32,9 @@ type Config struct {
|
||||
}
|
||||
|
||||
type NamedTunnelConfig struct {
|
||||
Credentials Credentials
|
||||
Client pogs.ClientInfo
|
||||
Credentials Credentials
|
||||
Client pogs.ClientInfo
|
||||
QuickTunnelUrl string
|
||||
}
|
||||
|
||||
// Credentials are stored in the credentials file and contain all info needed to run a tunnel.
|
||||
@@ -48,10 +59,6 @@ type ClassicTunnelConfig struct {
|
||||
UseReconnectToken bool
|
||||
}
|
||||
|
||||
func (c *ClassicTunnelConfig) IsTrialZone() bool {
|
||||
return c.Hostname == ""
|
||||
}
|
||||
|
||||
// Type indicates the connection type of the connection.
|
||||
type Type int
|
||||
|
||||
@@ -87,9 +94,64 @@ func (t Type) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// OriginProxy is how data flows from cloudflared to the origin services running behind it.
|
||||
type OriginProxy interface {
|
||||
// If Proxy returns an error, the caller is responsible for writing the error status to ResponseWriter
|
||||
Proxy(w ResponseWriter, req *http.Request, sourceConnectionType Type) error
|
||||
ProxyHTTP(w ResponseWriter, req *http.Request, isWebsocket bool) error
|
||||
ProxyTCP(ctx context.Context, rwa ReadWriteAcker, req *TCPRequest) error
|
||||
}
|
||||
|
||||
// TCPRequest defines the input format needed to perform a TCP proxy.
|
||||
type TCPRequest struct {
|
||||
Dest string
|
||||
CFRay string
|
||||
LBProbe bool
|
||||
}
|
||||
|
||||
// ReadWriteAcker is a readwriter with the ability to Acknowledge to the downstream (edge) that the origin has
|
||||
// accepted the connection.
|
||||
type ReadWriteAcker interface {
|
||||
io.ReadWriter
|
||||
AckConnection() error
|
||||
}
|
||||
|
||||
// HTTPResponseReadWriteAcker is an HTTP implementation of ReadWriteAcker.
|
||||
type HTTPResponseReadWriteAcker struct {
|
||||
r io.Reader
|
||||
w ResponseWriter
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
|
||||
func NewHTTPResponseReadWriterAcker(w ResponseWriter, req *http.Request) *HTTPResponseReadWriteAcker {
|
||||
return &HTTPResponseReadWriteAcker{
|
||||
r: req.Body,
|
||||
w: w,
|
||||
req: req,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
|
||||
return h.r.Read(p)
|
||||
}
|
||||
|
||||
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
|
||||
return h.w.Write(p)
|
||||
}
|
||||
|
||||
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
|
||||
// upgrade to streams.
|
||||
func (h *HTTPResponseReadWriteAcker) AckConnection() error {
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
ContentLength: -1,
|
||||
}
|
||||
|
||||
if secWebsocketKey := h.req.Header.Get("Sec-WebSocket-Key"); secWebsocketKey != "" {
|
||||
resp.Header = websocket.NewResponseHeader(h.req)
|
||||
}
|
||||
|
||||
return h.w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
}
|
||||
|
||||
type ResponseWriter interface {
|
||||
@@ -112,3 +174,11 @@ func IsServerSentEvent(headers http.Header) bool {
|
||||
func uint8ToString(input uint8) string {
|
||||
return strconv.FormatUint(uint64(input), 10)
|
||||
}
|
||||
|
||||
func FindCfRayHeader(req *http.Request) string {
|
||||
return req.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
func IsLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,7 +22,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
testConfig = &Config{
|
||||
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
|
||||
testConfig = &Config{
|
||||
OriginProxy: &mockOriginProxy{},
|
||||
GracePeriod: time.Millisecond * 100,
|
||||
}
|
||||
@@ -38,14 +43,25 @@ type testRequest struct {
|
||||
isProxyError bool
|
||||
}
|
||||
|
||||
type mockOriginProxy struct {
|
||||
}
|
||||
type mockOriginProxy struct{}
|
||||
|
||||
func (moc *mockOriginProxy) Proxy(w ResponseWriter, r *http.Request, sourceConnectionType Type) error {
|
||||
if sourceConnectionType == TypeWebsocket {
|
||||
return wsEndpoint(w, r)
|
||||
func (moc *mockOriginProxy) ProxyHTTP(
|
||||
w ResponseWriter,
|
||||
req *http.Request,
|
||||
isWebsocket bool,
|
||||
) error {
|
||||
if isWebsocket {
|
||||
switch req.URL.Path {
|
||||
case "/ws/echo":
|
||||
return wsEchoEndpoint(w, req)
|
||||
case "/ws/flaky":
|
||||
return wsFlakyEndpoint(w, req)
|
||||
default:
|
||||
originRespEndpoint(w, http.StatusNotFound, []byte("ws endpoint not found"))
|
||||
return fmt.Errorf("Unknwon websocket endpoint %s", req.URL.Path)
|
||||
}
|
||||
}
|
||||
switch r.URL.Path {
|
||||
switch req.URL.Path {
|
||||
case "/ok":
|
||||
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
||||
case "/large_file":
|
||||
@@ -60,34 +76,93 @@ func (moc *mockOriginProxy) Proxy(w ResponseWriter, r *http.Request, sourceConne
|
||||
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
type nowriter struct {
|
||||
io.Reader
|
||||
func (moc *mockOriginProxy) ProxyTCP(
|
||||
ctx context.Context,
|
||||
rwa ReadWriteAcker,
|
||||
r *TCPRequest,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nowriter) Write(p []byte) (int, error) {
|
||||
return 0, fmt.Errorf("Writer not implemented")
|
||||
type echoPipe struct {
|
||||
reader *io.PipeReader
|
||||
writer *io.PipeWriter
|
||||
}
|
||||
|
||||
func wsEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
func (ep *echoPipe) Read(p []byte) (int, error) {
|
||||
return ep.reader.Read(p)
|
||||
}
|
||||
|
||||
func (ep *echoPipe) Write(p []byte) (int, error) {
|
||||
return ep.writer.Write(p)
|
||||
}
|
||||
|
||||
// A mock origin that echos data by streaming like a tcpOverWSConnection
|
||||
// https://github.com/cloudflare/cloudflared/blob/master/ingress/origin_connection.go
|
||||
func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
clientReader := nowriter{r.Body}
|
||||
if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
wsCtx, cancel := context.WithCancel(r.Context())
|
||||
readPipe, writePipe := io.Pipe()
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
||||
go func() {
|
||||
for {
|
||||
data, err := wsutil.ReadClientText(clientReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := wsutil.WriteServerText(w, data); err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-wsCtx.Done():
|
||||
case <-r.Context().Done():
|
||||
}
|
||||
readPipe.Close()
|
||||
writePipe.Close()
|
||||
}()
|
||||
<-r.Context().Done()
|
||||
|
||||
originConn := &echoPipe{reader: readPipe, writer: writePipe}
|
||||
websocket.Stream(wsConn, originConn, &log)
|
||||
cancel()
|
||||
wsConn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
type flakyConn struct {
|
||||
closeAt time.Time
|
||||
}
|
||||
|
||||
func (fc *flakyConn) Read(p []byte) (int, error) {
|
||||
if time.Now().After(fc.closeAt) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, []byte("Read from flaky connection"))
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (fc *flakyConn) Write(p []byte) (int, error) {
|
||||
if time.Now().After(fc.closeAt) {
|
||||
return 0, fmt.Errorf("Flaky connection closed")
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
}
|
||||
if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
wsCtx, cancel := context.WithCancel(r.Context())
|
||||
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
||||
|
||||
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
|
||||
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
|
||||
websocket.Stream(wsConn, originConn, &log)
|
||||
cancel()
|
||||
wsConn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// RPCClientFunc derives a named tunnel rpc client that can then be used to register and unregister connections.
|
||||
type RPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
type controlStream struct {
|
||||
observer *Observer
|
||||
|
||||
connectedFuse ConnectedFuse
|
||||
namedTunnelConfig *NamedTunnelConfig
|
||||
connIndex uint8
|
||||
|
||||
newRPCClientFunc RPCClientFunc
|
||||
|
||||
gracefulShutdownC <-chan struct{}
|
||||
gracePeriod time.Duration
|
||||
stoppedGracefully bool
|
||||
}
|
||||
|
||||
// ControlStreamHandler registers connections with origintunneld and initiates graceful shutdown.
|
||||
type ControlStreamHandler interface {
|
||||
ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, shouldWaitForUnregister bool) error
|
||||
IsStopped() bool
|
||||
}
|
||||
|
||||
// NewControlStream returns a new instance of ControlStreamHandler
|
||||
func NewControlStream(
|
||||
observer *Observer,
|
||||
connectedFuse ConnectedFuse,
|
||||
namedTunnelConfig *NamedTunnelConfig,
|
||||
connIndex uint8,
|
||||
newRPCClientFunc RPCClientFunc,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
gracePeriod time.Duration,
|
||||
) ControlStreamHandler {
|
||||
if newRPCClientFunc == nil {
|
||||
newRPCClientFunc = newRegistrationRPCClient
|
||||
}
|
||||
return &controlStream{
|
||||
observer: observer,
|
||||
connectedFuse: connectedFuse,
|
||||
namedTunnelConfig: namedTunnelConfig,
|
||||
newRPCClientFunc: newRPCClientFunc,
|
||||
connIndex: connIndex,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
gracePeriod: gracePeriod,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controlStream) ServeControlStream(
|
||||
ctx context.Context,
|
||||
rw io.ReadWriteCloser,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
shouldWaitForUnregister bool,
|
||||
) error {
|
||||
rpcClient := c.newRPCClientFunc(ctx, rw, c.observer.log)
|
||||
|
||||
if err := rpcClient.RegisterConnection(ctx, c.namedTunnelConfig, connOptions, c.connIndex, c.observer); err != nil {
|
||||
rpcClient.Close()
|
||||
return err
|
||||
}
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
if shouldWaitForUnregister {
|
||||
c.waitForUnregister(ctx, rpcClient)
|
||||
} else {
|
||||
go c.waitForUnregister(ctx, rpcClient)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controlStream) waitForUnregister(ctx context.Context, rpcClient NamedTunnelRPCClient) {
|
||||
// wait for connection termination or start of graceful shutdown
|
||||
defer rpcClient.Close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-c.gracefulShutdownC:
|
||||
c.stoppedGracefully = true
|
||||
}
|
||||
|
||||
c.observer.sendUnregisteringEvent(c.connIndex)
|
||||
rpcClient.GracefulShutdown(ctx, c.gracePeriod)
|
||||
c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Unregistered tunnel connection")
|
||||
}
|
||||
|
||||
func (c *controlStream) IsStopped() bool {
|
||||
return c.stoppedGracefully
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@ const (
|
||||
Connected
|
||||
// Reconnecting means the connection to the edge is being re-established.
|
||||
Reconnecting
|
||||
// SetURL means this connection's tunnel was given a URL by the edge. Used for free tunnels.
|
||||
// SetURL means this connection's tunnel was given a URL by the edge. Used for quick tunnels.
|
||||
SetURL
|
||||
// RegisteringTunnel means the non-named tunnel is registering its connection.
|
||||
RegisteringTunnel
|
||||
|
||||
+7
-5
@@ -33,6 +33,8 @@ type h2muxConnection struct {
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
}
|
||||
@@ -167,7 +169,8 @@ func (h *h2muxConnection) serveMuxer(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) controlLoop(ctx context.Context, connectedFuse ConnectedFuse, isNamedTunnel bool) {
|
||||
updateMetricsTickC := time.Tick(h.muxerConfig.MetricsUpdateFreq)
|
||||
updateMetricsTicker := time.NewTicker(h.muxerConfig.MetricsUpdateFreq)
|
||||
defer updateMetricsTicker.Stop()
|
||||
var shutdownCompleted <-chan struct{}
|
||||
for {
|
||||
select {
|
||||
@@ -191,7 +194,7 @@ func (h *h2muxConnection) controlLoop(ctx context.Context, connectedFuse Connect
|
||||
// don't wait for shutdown to finish when context is closed, this is the hard termination path
|
||||
return
|
||||
|
||||
case <-updateMetricsTickC:
|
||||
case <-updateMetricsTicker.C:
|
||||
h.observer.metrics.updateMuxerMetrics(h.connIndexStr, h.muxer.Metrics())
|
||||
}
|
||||
}
|
||||
@@ -221,12 +224,11 @@ func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
sourceConnectionType = TypeWebsocket
|
||||
}
|
||||
|
||||
err := h.config.OriginProxy.Proxy(respWriter, req, sourceConnectionType)
|
||||
err := h.config.OriginProxy.ProxyHTTP(respWriter, req, sourceConnectionType == TypeWebsocket)
|
||||
if err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *h2muxConnection) newRequest(stream *h2mux.MuxedStream) (*http.Request, error) {
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestServeStreamWS(t *testing.T) {
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: "/ws",
|
||||
Value: "/ws/echo",
|
||||
},
|
||||
{
|
||||
Name: "connection",
|
||||
@@ -167,10 +167,10 @@ func TestServeStreamWS(t *testing.T) {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, data)
|
||||
err = wsutil.WriteClientBinary(writePipe, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
respBody, err := wsutil.ReadServerText(stream)
|
||||
respBody, err := wsutil.ReadServerBinary(stream)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
|
||||
|
||||
|
||||
+11
-4
@@ -54,7 +54,7 @@ const ()
|
||||
func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
for _, header := range h2 {
|
||||
name := strings.ToLower(header.Name)
|
||||
if !IsControlHeader(name) {
|
||||
if !IsControlRequestHeader(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -121,13 +121,20 @@ func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsControlHeader(headerName string) bool {
|
||||
func IsControlRequestHeader(headerName string) bool {
|
||||
return headerName == "content-length" ||
|
||||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
|
||||
headerName == "connection" || headerName == "upgrade" || // Websocket request headers
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-")
|
||||
}
|
||||
|
||||
func IsControlResponseHeader(headerName string) bool {
|
||||
return headerName == "content-length" ||
|
||||
strings.HasPrefix(headerName, ":") ||
|
||||
strings.HasPrefix(headerName, "cf-int-") ||
|
||||
strings.HasPrefix(headerName, "cf-cloudflared-")
|
||||
}
|
||||
|
||||
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
||||
func IsWebsocketClientHeader(headerName string) bool {
|
||||
return headerName == "sec-websocket-accept" ||
|
||||
@@ -148,7 +155,7 @@ func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []h2mux.Heade
|
||||
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2 = append(h2, h2mux.Header{Name: "content-length", Value: values[0]})
|
||||
} else if !IsControlHeader(h2name) || IsWebsocketClientHeader(h2name) {
|
||||
} else if !IsControlResponseHeader(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
|
||||
|
||||
+53
-13
@@ -511,7 +511,7 @@ func TestDeserializeMalformed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHeaders(t *testing.T) {
|
||||
func TestParseRequestHeaders(t *testing.T) {
|
||||
mockUserHeadersToSerialize := http.Header{
|
||||
"Mock-Header-One": {"1", "1.5"},
|
||||
"Mock-Header-Two": {"2"},
|
||||
@@ -541,8 +541,8 @@ func TestParseHeaders(t *testing.T) {
|
||||
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
|
||||
}
|
||||
|
||||
func TestIsControlHeader(t *testing.T) {
|
||||
controlHeaders := []string{
|
||||
func TestIsControlRequestHeader(t *testing.T) {
|
||||
controlRequestHeaders := []string{
|
||||
// Anything that begins with cf-
|
||||
"cf-sample-header",
|
||||
|
||||
@@ -552,30 +552,69 @@ func TestIsControlHeader(t *testing.T) {
|
||||
// content-length is a special case, it has to be there
|
||||
// for some requests to work (per the HTTP2 spec)
|
||||
"content-length",
|
||||
|
||||
// Websocket request headers
|
||||
"connection",
|
||||
"upgrade",
|
||||
}
|
||||
|
||||
for _, header := range controlHeaders {
|
||||
assert.True(t, IsControlHeader(header))
|
||||
for _, header := range controlRequestHeaders {
|
||||
assert.True(t, IsControlRequestHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotControlHeader(t *testing.T) {
|
||||
notControlHeaders := []string{
|
||||
func TestIsControlResponseHeader(t *testing.T) {
|
||||
controlResponseHeaders := []string{
|
||||
// Anything that begins with cf-int- or cf-cloudflared-
|
||||
"cf-int-sample-header",
|
||||
"cf-cloudflared-sample-header",
|
||||
|
||||
// Any http2 pseudoheader
|
||||
":sample-pseudo-header",
|
||||
|
||||
// content-length is a special case, it has to be there
|
||||
// for some requests to work (per the HTTP2 spec)
|
||||
"content-length",
|
||||
}
|
||||
|
||||
for _, header := range controlResponseHeaders {
|
||||
assert.True(t, IsControlResponseHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotControlRequestHeader(t *testing.T) {
|
||||
notControlRequestHeaders := []string{
|
||||
"mock-header",
|
||||
"another-sample-header",
|
||||
}
|
||||
|
||||
for _, header := range notControlHeaders {
|
||||
assert.False(t, IsControlHeader(header))
|
||||
for _, header := range notControlRequestHeaders {
|
||||
assert.False(t, IsControlRequestHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNotControlResponseHeader(t *testing.T) {
|
||||
notControlResponseHeaders := []string{
|
||||
"mock-header",
|
||||
"another-sample-header",
|
||||
"upgrade",
|
||||
"connection",
|
||||
"cf-whatever", // On the response path, we only want to filter cf-int- and cf-cloudflared-
|
||||
}
|
||||
|
||||
for _, header := range notControlResponseHeaders {
|
||||
assert.False(t, IsControlResponseHeader(header))
|
||||
}
|
||||
}
|
||||
|
||||
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
mockHeaders := http.Header{
|
||||
"User-header-one": {""},
|
||||
"User-header-two": {"1", "2"},
|
||||
"cf-header": {"cf-value"},
|
||||
"Content-Length": {"123"},
|
||||
"User-header-one": {""},
|
||||
"User-header-two": {"1", "2"},
|
||||
"cf-header": {"cf-value"},
|
||||
"cf-int-header": {"cf-int-value"},
|
||||
"cf-cloudflared-header": {"cf-cloudflared-value"},
|
||||
"Content-Length": {"123"},
|
||||
}
|
||||
mockResponse := http.Response{
|
||||
StatusCode: 200,
|
||||
@@ -608,6 +647,7 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
{Name: "User-header-one", Value: ""},
|
||||
{Name: "User-header-two", Value: "1"},
|
||||
{Name: "User-header-two", Value: "2"},
|
||||
{Name: "cf-header", Value: "cf-value"},
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
|
||||
|
||||
+97
-69
@@ -4,12 +4,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
@@ -26,53 +27,52 @@ const (
|
||||
|
||||
var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
|
||||
|
||||
type http2Connection struct {
|
||||
conn net.Conn
|
||||
server *http2.Server
|
||||
config *Config
|
||||
namedTunnel *NamedTunnelConfig
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndexStr string
|
||||
connIndex uint8
|
||||
// HTTP2Connection represents a net.Conn that uses HTTP2 frames to proxy traffic from the edge to cloudflared on the
|
||||
// origin.
|
||||
type HTTP2Connection struct {
|
||||
conn net.Conn
|
||||
server *http2.Server
|
||||
config *Config
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndex uint8
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
activeRequestsWG sync.WaitGroup
|
||||
connectedFuse ConnectedFuse
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
controlStreamErr error // result of running control stream handler
|
||||
log *zerolog.Logger
|
||||
activeRequestsWG sync.WaitGroup
|
||||
controlStreamHandler ControlStreamHandler
|
||||
stoppedGracefully bool
|
||||
controlStreamErr error // result of running control stream handler
|
||||
}
|
||||
|
||||
// NewHTTP2Connection returns a new instance of HTTP2Connection.
|
||||
func NewHTTP2Connection(
|
||||
conn net.Conn,
|
||||
config *Config,
|
||||
namedTunnelConfig *NamedTunnelConfig,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
observer *Observer,
|
||||
connIndex uint8,
|
||||
connectedFuse ConnectedFuse,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) *http2Connection {
|
||||
return &http2Connection{
|
||||
controlStreamHandler ControlStreamHandler,
|
||||
log *zerolog.Logger,
|
||||
) *HTTP2Connection {
|
||||
return &HTTP2Connection{
|
||||
conn: conn,
|
||||
server: &http2.Server{
|
||||
MaxConcurrentStreams: math.MaxUint32,
|
||||
MaxConcurrentStreams: MaxConcurrentStreams,
|
||||
},
|
||||
config: config,
|
||||
namedTunnel: namedTunnelConfig,
|
||||
connOptions: connOptions,
|
||||
observer: observer,
|
||||
connIndexStr: uint8ToString(connIndex),
|
||||
connIndex: connIndex,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
connectedFuse: connectedFuse,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
config: config,
|
||||
connOptions: connOptions,
|
||||
observer: observer,
|
||||
connIndex: connIndex,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
controlStreamHandler: controlStreamHandler,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) Serve(ctx context.Context) error {
|
||||
// Serve serves an HTTP2 server that the edge can talk to.
|
||||
func (c *HTTP2Connection) Serve(ctx context.Context) error {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.close()
|
||||
@@ -83,7 +83,7 @@ func (c *http2Connection) Serve(ctx context.Context) error {
|
||||
})
|
||||
|
||||
switch {
|
||||
case c.stoppedGracefully:
|
||||
case c.controlStreamHandler.IsStopped():
|
||||
return nil
|
||||
case c.controlStreamErr != nil:
|
||||
return c.controlStreamErr
|
||||
@@ -93,57 +93,60 @@ func (c *http2Connection) Serve(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.activeRequestsWG.Add(1)
|
||||
defer c.activeRequestsWG.Done()
|
||||
|
||||
connType := determineHTTP2Type(r)
|
||||
respWriter, err := newHTTP2RespWriter(r, w, connType)
|
||||
handleMissingRequestParts(connType, r)
|
||||
|
||||
respWriter, err := NewHTTP2RespWriter(r, w, connType)
|
||||
if err != nil {
|
||||
c.observer.log.Error().Msg(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var proxyErr error
|
||||
switch connType {
|
||||
case TypeControlStream:
|
||||
proxyErr = c.serveControlStream(r.Context(), respWriter)
|
||||
c.controlStreamErr = proxyErr
|
||||
case TypeWebsocket:
|
||||
if err := c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions, true); err != nil {
|
||||
c.controlStreamErr = err
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
case TypeWebsocket, TypeHTTP:
|
||||
stripWebsocketUpgradeHeader(r)
|
||||
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, TypeWebsocket)
|
||||
if err := c.config.OriginProxy.ProxyHTTP(respWriter, r, connType == TypeWebsocket); err != nil {
|
||||
err := fmt.Errorf("Failed to proxy HTTP: %w", err)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
case TypeTCP:
|
||||
host, err := getRequestHost(r)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(`cloudflared recieved a warp-routing request with an empty host value: %w`, err)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
|
||||
if err := c.config.OriginProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
Dest: host,
|
||||
CFRay: FindCfRayHeader(r),
|
||||
LBProbe: IsLBProbeRequest(r),
|
||||
}); err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
default:
|
||||
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, connType)
|
||||
}
|
||||
if proxyErr != nil {
|
||||
err := fmt.Errorf("Received unknown connection type: %s", connType)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *http2Connection) serveControlStream(ctx context.Context, respWriter *http2RespWriter) error {
|
||||
rpcClient := c.newRPCClientFunc(ctx, respWriter, c.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
if err := rpcClient.RegisterConnection(ctx, c.namedTunnel, c.connOptions, c.connIndex, c.observer); err != nil {
|
||||
return err
|
||||
}
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
// wait for connection termination or start of graceful shutdown
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-c.gracefulShutdownC:
|
||||
c.stoppedGracefully = true
|
||||
}
|
||||
|
||||
c.observer.sendUnregisteringEvent(c.connIndex)
|
||||
rpcClient.GracefulShutdown(ctx, c.config.GracePeriod)
|
||||
c.observer.log.Info().Uint8(LogFieldConnIndex, c.connIndex).Msg("Unregistered tunnel connection")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http2Connection) close() {
|
||||
func (c *HTTP2Connection) close() {
|
||||
// Wait for all serve HTTP handlers to return
|
||||
c.activeRequestsWG.Wait()
|
||||
c.conn.Close()
|
||||
@@ -156,7 +159,7 @@ type http2RespWriter struct {
|
||||
shouldFlush bool
|
||||
}
|
||||
|
||||
func newHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
|
||||
func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
|
||||
flusher, isFlusher := w.(http.Flusher)
|
||||
if !isFlusher {
|
||||
respWriter := &http2RespWriter{
|
||||
@@ -186,7 +189,7 @@ func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) erro
|
||||
// so it should be sent as an HTTP/2 response header.
|
||||
dest[name] = values
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
} else if !IsControlHeader(h2name) || IsWebsocketClientHeader(h2name) {
|
||||
} else if !IsControlResponseHeader(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[name] = values
|
||||
@@ -228,7 +231,7 @@ func (rp *http2RespWriter) Write(p []byte) (n int, err error) {
|
||||
// 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)
|
||||
println(fmt.Sprintf("Recover from http2 response writer panic, error %s", debug.Stack()))
|
||||
}
|
||||
}()
|
||||
n, err = rp.w.Write(p)
|
||||
@@ -255,6 +258,20 @@ func determineHTTP2Type(r *http.Request) Type {
|
||||
}
|
||||
}
|
||||
|
||||
func handleMissingRequestParts(connType Type, r *http.Request) {
|
||||
if connType == TypeHTTP {
|
||||
// http library has no guarantees that we receive a filled URL. If not, then we fill it, as we reuse the request
|
||||
// for proxying. We use the same values as we used to in h2mux. For proxying they should not matter since we
|
||||
// control the dialer on every egress proxied.
|
||||
if len(r.URL.Scheme) == 0 {
|
||||
r.URL.Scheme = "http"
|
||||
}
|
||||
if len(r.URL.Host) == 0 {
|
||||
r.URL.Host = "localhost:8080"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isControlStreamUpgrade(r *http.Request) bool {
|
||||
return r.Header.Get(InternalUpgradeHeader) == ControlStreamUpgrade
|
||||
}
|
||||
@@ -271,3 +288,14 @@ func IsTCPStream(r *http.Request) bool {
|
||||
func stripWebsocketUpgradeHeader(r *http.Request) {
|
||||
r.Header.Del(InternalUpgradeHeader)
|
||||
}
|
||||
|
||||
// getRequestHost returns the host of the http.Request.
|
||||
func getRequestHost(r *http.Request) (string, error) {
|
||||
if r.Host != "" {
|
||||
return r.Host, nil
|
||||
}
|
||||
if r.URL != nil {
|
||||
return r.URL.Host, nil
|
||||
}
|
||||
return "", errors.New("host not set in incoming request")
|
||||
}
|
||||
|
||||
+140
-26
@@ -26,18 +26,29 @@ var (
|
||||
testTransport = http2.Transport{}
|
||||
)
|
||||
|
||||
func newTestHTTP2Connection() (*http2Connection, net.Conn) {
|
||||
edgeConn, originConn := net.Pipe()
|
||||
func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
|
||||
edgeConn, cfdConn := net.Pipe()
|
||||
var connIndex = uint8(0)
|
||||
return NewHTTP2Connection(
|
||||
originConn,
|
||||
testConfig,
|
||||
&NamedTunnelConfig{},
|
||||
&pogs.ConnectionOptions{},
|
||||
NewObserver(&log, &log, false),
|
||||
connIndex,
|
||||
log := zerolog.Nop()
|
||||
obs := NewObserver(&log, &log, false)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
connIndex,
|
||||
nil,
|
||||
nil,
|
||||
1*time.Second,
|
||||
)
|
||||
return NewHTTP2Connection(
|
||||
cfdConn,
|
||||
// OriginProxy is set in testConfig
|
||||
testConfig,
|
||||
&pogs.ConnectionOptions{},
|
||||
obs,
|
||||
connIndex,
|
||||
controlStream,
|
||||
&log,
|
||||
), edgeConn
|
||||
}
|
||||
|
||||
@@ -156,6 +167,7 @@ type wsRespWriter struct {
|
||||
*httptest.ResponseRecorder
|
||||
readPipe *io.PipeReader
|
||||
writePipe *io.PipeWriter
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newWSRespWriter() *wsRespWriter {
|
||||
@@ -164,46 +176,58 @@ func newWSRespWriter() *wsRespWriter {
|
||||
httptest.NewRecorder(),
|
||||
readPipe,
|
||||
writePipe,
|
||||
false,
|
||||
}
|
||||
}
|
||||
|
||||
type nowriter struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (nowriter) Write(p []byte) (int, error) {
|
||||
return 0, fmt.Errorf("Writer not implemented")
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) RespBody() io.ReadWriter {
|
||||
return nowriter{w.readPipe}
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) Write(data []byte) (n int, err error) {
|
||||
if w.closed {
|
||||
// Simulate writing to http2 ResponseWriter after ServeHTTP has returned
|
||||
panic("Write to closed ResponseWriter")
|
||||
}
|
||||
return w.writePipe.Write(data)
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) close() {
|
||||
w.closed = true
|
||||
}
|
||||
|
||||
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)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/ws/echo", readPipe)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade)
|
||||
|
||||
wg.Add(1)
|
||||
serveDone := make(chan struct{})
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(serveDone)
|
||||
http2Conn.ServeHTTP(respWriter, req)
|
||||
respWriter.close()
|
||||
}()
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, data)
|
||||
err = wsutil.WriteClientBinary(writePipe, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
respBody, err := wsutil.ReadServerText(respWriter.RespBody())
|
||||
respBody, err := wsutil.ReadServerBinary(respWriter.RespBody())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
|
||||
|
||||
@@ -213,7 +237,64 @@ func TestServeWS(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader))
|
||||
|
||||
<-serveDone
|
||||
}
|
||||
|
||||
// TestNoWriteAfterServeHTTPReturns is a regression test of https://jira.cfops.it/browse/TUN-5184
|
||||
// to make sure we don't write to the ResponseWriter after the ServeHTTP method returns
|
||||
func TestNoWriteAfterServeHTTPReturns(t *testing.T) {
|
||||
cfdHTTP2Conn, edgeTCPConn := newTestHTTP2Connection()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
|
||||
serverDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(serverDone)
|
||||
cfdHTTP2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
edgeTransport := http2.Transport{}
|
||||
edgeHTTP2Conn, err := edgeTransport.NewClientConn(edgeTCPConn)
|
||||
require.NoError(t, err)
|
||||
message := []byte(t.Name())
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
readPipe, writePipe := io.Pipe()
|
||||
reqCtx, reqCancel := context.WithCancel(ctx)
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, "http://localhost:8080/ws/flaky", readPipe)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade)
|
||||
|
||||
resp, err := edgeHTTP2Conn.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
// http2RespWriter should rewrite status 101 to 200
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-reqCtx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
_ = wsutil.WriteClientBinary(writePipe, message)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
reqCancel()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
cancel()
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestServeControlStream(t *testing.T) {
|
||||
@@ -223,7 +304,18 @@ func TestServeControlStream(t *testing.T) {
|
||||
registered: make(chan struct{}),
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
|
||||
obs := NewObserver(&log, &log, false)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
1,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
nil,
|
||||
1*time.Second,
|
||||
)
|
||||
http2Conn.controlStreamHandler = controlStream
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
@@ -262,7 +354,18 @@ func TestFailRegistration(t *testing.T) {
|
||||
registered: make(chan struct{}),
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
|
||||
obs := NewObserver(&log, &log, false)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
http2Conn.connIndex,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
nil,
|
||||
1*time.Second,
|
||||
)
|
||||
http2Conn.controlStreamHandler = controlStream
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
@@ -295,10 +398,21 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
unregistered: make(chan struct{}),
|
||||
}
|
||||
events := &eventCollectorSink{}
|
||||
http2Conn.newRPCClientFunc = rpcClientFactory.newMockRPCClient
|
||||
http2Conn.observer.RegisterSink(events)
|
||||
|
||||
shutdownC := make(chan struct{})
|
||||
http2Conn.gracefulShutdownC = shutdownC
|
||||
obs := NewObserver(&log, &log, false)
|
||||
obs.RegisterSink(events)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
http2Conn.connIndex,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
shutdownC,
|
||||
1*time.Second,
|
||||
)
|
||||
|
||||
http2Conn.controlStreamHandler = controlStream
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
@@ -337,7 +451,7 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
case <-time.Tick(time.Second):
|
||||
t.Fatal("timeout out waiting for unregistered signal")
|
||||
}
|
||||
assert.True(t, http2Conn.stoppedGracefully)
|
||||
assert.True(t, controlStream.IsStopped())
|
||||
|
||||
cancel()
|
||||
wg.Wait()
|
||||
|
||||
+8
-52
@@ -1,13 +1,9 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,53 +50,6 @@ func (o *Observer) logServerInfo(connIndex uint8, location, msg string) {
|
||||
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.uiEnabled {
|
||||
if registrationURL, err := url.Parse(registration.Url); err == nil {
|
||||
for _, line := range asciiBox(trialZoneMsg(registrationURL.String()), 2) {
|
||||
o.log.Info().Msg(line)
|
||||
}
|
||||
} else {
|
||||
o.log.Error().Msg("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(connIndex uint8) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel})
|
||||
}
|
||||
@@ -109,8 +58,15 @@ func (o *Observer) sendConnectedEvent(connIndex uint8, location string) {
|
||||
o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location})
|
||||
}
|
||||
|
||||
func (o *Observer) sendURL(url string) {
|
||||
func (o *Observer) SendURL(url string) {
|
||||
o.sendEvent(Event{EventType: SetURL, URL: url})
|
||||
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
// We add https:// in the prefix for backwards compatibility as we used to do that with the old free tunnels
|
||||
// and some tools (like `wrangler tail`) are regexp-ing for that specifically.
|
||||
url = "https://" + url
|
||||
}
|
||||
o.metrics.userHostnamesCounts.WithLabelValues(url).Inc()
|
||||
}
|
||||
|
||||
func (o *Observer) SendReconnect(connIndex uint8) {
|
||||
|
||||
@@ -6,9 +6,28 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSendUrl(t *testing.T) {
|
||||
observer := NewObserver(&log, &log, false)
|
||||
|
||||
observer.SendURL("my-url.com")
|
||||
assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://my-url.com"))
|
||||
|
||||
observer.SendURL("https://another-long-one.com")
|
||||
assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://another-long-one.com"))
|
||||
}
|
||||
|
||||
func getCounterValue(t *testing.T, metric *prometheus.CounterVec, val string) float64 {
|
||||
var m = &dto.Metric{}
|
||||
err := metric.WithLabelValues(val).Write(m)
|
||||
assert.NoError(t, err)
|
||||
return m.Counter.GetValue()
|
||||
}
|
||||
|
||||
func TestRegisterServerLocation(t *testing.T) {
|
||||
m := newTunnelMetrics()
|
||||
tunnels := 20
|
||||
|
||||
+140
-50
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -15,33 +17,35 @@ const (
|
||||
edgeH2muxTLSServerName = "cftunnel.com"
|
||||
// edgeH2TLSServerName is the server name to establish http2 connection with edge
|
||||
edgeH2TLSServerName = "h2.cftunnel.com"
|
||||
// edgeQUICServerName is the server name to establish quic connection with edge.
|
||||
edgeQUICServerName = "quic.cftunnel.com"
|
||||
// threshold to switch back to h2mux when the user intentionally pick --protocol http2
|
||||
explicitHTTP2FallbackThreshold = -1
|
||||
autoSelectFlag = "auto"
|
||||
)
|
||||
|
||||
var (
|
||||
ProtocolList = []Protocol{H2mux, HTTP2}
|
||||
// ProtocolList represents a list of supported protocols for communication with the edge.
|
||||
ProtocolList = []Protocol{H2mux, HTTP2, HTTP2Warp, QUIC, QUICWarp}
|
||||
)
|
||||
|
||||
type Protocol int64
|
||||
|
||||
const (
|
||||
// H2mux protocol can be used both with Classic and Named Tunnels. .
|
||||
H2mux Protocol = iota
|
||||
// HTTP2 is used only with named tunnels. It's more efficient than H2mux for L4 proxying.
|
||||
HTTP2
|
||||
// QUIC is used only with named tunnels.
|
||||
QUIC
|
||||
// HTTP2Warp is used only with named tunnels. It's useful for warp-routing where we don't want to fallback to
|
||||
// H2mux on HTTP2 failure to connect.
|
||||
HTTP2Warp
|
||||
//QUICWarp is used only with named tunnels. It's useful for warp-routing where we want to fallback to HTTP2 but
|
||||
// dont' want HTTP2 to fallback to H2mux
|
||||
QUICWarp
|
||||
)
|
||||
|
||||
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 {
|
||||
@@ -49,6 +53,12 @@ func (p Protocol) fallback() (Protocol, bool) {
|
||||
return 0, false
|
||||
case HTTP2:
|
||||
return H2mux, true
|
||||
case HTTP2Warp:
|
||||
return 0, false
|
||||
case QUIC:
|
||||
return HTTP2, true
|
||||
case QUICWarp:
|
||||
return HTTP2Warp, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
@@ -58,13 +68,40 @@ func (p Protocol) String() string {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return "h2mux"
|
||||
case HTTP2:
|
||||
case HTTP2, HTTP2Warp:
|
||||
return "http2"
|
||||
case QUIC, QUICWarp:
|
||||
return "quic"
|
||||
default:
|
||||
return fmt.Sprintf("unknown protocol")
|
||||
}
|
||||
}
|
||||
|
||||
func (p Protocol) TLSSettings() *TLSSettings {
|
||||
switch p {
|
||||
case H2mux:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeH2muxTLSServerName,
|
||||
}
|
||||
case HTTP2, HTTP2Warp:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeH2TLSServerName,
|
||||
}
|
||||
case QUIC, QUICWarp:
|
||||
return &TLSSettings{
|
||||
ServerName: edgeQUICServerName,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type TLSSettings struct {
|
||||
ServerName string
|
||||
NextProtos []string
|
||||
}
|
||||
|
||||
type ProtocolSelector interface {
|
||||
Current() Protocol
|
||||
Fallback() (Protocol, bool)
|
||||
@@ -83,29 +120,36 @@ func (s *staticProtocolSelector) Fallback() (Protocol, bool) {
|
||||
}
|
||||
|
||||
type autoProtocolSelector struct {
|
||||
lock sync.RWMutex
|
||||
current Protocol
|
||||
switchThrehold int32
|
||||
fetchFunc PercentageFetcher
|
||||
refreshAfter time.Time
|
||||
ttl time.Duration
|
||||
log *zerolog.Logger
|
||||
lock sync.RWMutex
|
||||
|
||||
current Protocol
|
||||
|
||||
// protocolPool is desired protocols in the order of priority they should be picked in.
|
||||
protocolPool []Protocol
|
||||
|
||||
switchThreshold int32
|
||||
fetchFunc PercentageFetcher
|
||||
refreshAfter time.Time
|
||||
ttl time.Duration
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func newAutoProtocolSelector(
|
||||
current Protocol,
|
||||
switchThrehold int32,
|
||||
protocolPool []Protocol,
|
||||
switchThreshold int32,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) *autoProtocolSelector {
|
||||
return &autoProtocolSelector{
|
||||
current: current,
|
||||
switchThrehold: switchThrehold,
|
||||
fetchFunc: fetchFunc,
|
||||
refreshAfter: time.Now().Add(ttl),
|
||||
ttl: ttl,
|
||||
log: log,
|
||||
current: current,
|
||||
protocolPool: protocolPool,
|
||||
switchThreshold: switchThreshold,
|
||||
fetchFunc: fetchFunc,
|
||||
refreshAfter: time.Now().Add(ttl),
|
||||
ttl: ttl,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,28 +160,39 @@ func (s *autoProtocolSelector) Current() Protocol {
|
||||
return s.current
|
||||
}
|
||||
|
||||
percentage, err := s.fetchFunc()
|
||||
protocol, err := getProtocol(s.protocolPool, s.fetchFunc, s.switchThreshold)
|
||||
if err != nil {
|
||||
s.log.Err(err).Msg("Failed to refresh protocol")
|
||||
return s.current
|
||||
}
|
||||
s.current = protocol
|
||||
|
||||
if s.switchThrehold < percentage {
|
||||
s.current = HTTP2
|
||||
} else {
|
||||
s.current = H2mux
|
||||
}
|
||||
s.refreshAfter = time.Now().Add(s.ttl)
|
||||
return s.current
|
||||
}
|
||||
|
||||
func getProtocol(protocolPool []Protocol, fetchFunc PercentageFetcher, switchThreshold int32) (Protocol, error) {
|
||||
protocolPercentages, err := fetchFunc()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, protocol := range protocolPool {
|
||||
protocolPercentage := protocolPercentages.GetPercentage(protocol.String())
|
||||
if protocolPercentage > switchThreshold {
|
||||
return protocol, nil
|
||||
}
|
||||
}
|
||||
|
||||
return protocolPool[len(protocolPool)-1], nil
|
||||
}
|
||||
|
||||
func (s *autoProtocolSelector) Fallback() (Protocol, bool) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.current.fallback()
|
||||
}
|
||||
|
||||
type PercentageFetcher func() (int32, error)
|
||||
type PercentageFetcher func() (edgediscovery.ProtocolPercents, error)
|
||||
|
||||
func NewProtocolSelector(
|
||||
protocolFlag string,
|
||||
@@ -154,41 +209,76 @@ func NewProtocolSelector(
|
||||
}, nil
|
||||
}
|
||||
|
||||
// warp routing can only be served over http2 connections
|
||||
if warpRoutingEnabled {
|
||||
if protocolFlag == H2mux.String() {
|
||||
log.Warn().Msg("Warp routing is only supported by http2 protocol. Upgrading protocol to http2")
|
||||
}
|
||||
threshold := switchThreshold(namedTunnel.Credentials.AccountTag)
|
||||
fetchedProtocol, err := getProtocol([]Protocol{QUIC, HTTP2}, fetchFunc, threshold)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Unable to lookup protocol. Defaulting to `http2`. If this fails, you can set `--protocol h2mux` in your cloudflared command.")
|
||||
return &staticProtocolSelector{
|
||||
current: HTTP2,
|
||||
}, nil
|
||||
}
|
||||
if warpRoutingEnabled {
|
||||
if protocolFlag == H2mux.String() || fetchedProtocol == H2mux {
|
||||
log.Warn().Msg("Warp routing is not supported in h2mux protocol. Upgrading to http2 to allow it.")
|
||||
protocolFlag = HTTP2.String()
|
||||
fetchedProtocol = HTTP2Warp
|
||||
}
|
||||
return selectWarpRoutingProtocols(protocolFlag, fetchFunc, ttl, log, threshold, fetchedProtocol)
|
||||
}
|
||||
|
||||
return selectNamedTunnelProtocols(protocolFlag, fetchFunc, ttl, log, threshold, fetchedProtocol)
|
||||
}
|
||||
|
||||
func selectNamedTunnelProtocols(
|
||||
protocolFlag string,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
threshold int32,
|
||||
protocol Protocol,
|
||||
) (ProtocolSelector, error) {
|
||||
if protocolFlag == H2mux.String() {
|
||||
return &staticProtocolSelector{
|
||||
current: H2mux,
|
||||
}, nil
|
||||
}
|
||||
|
||||
http2Percentage, err := fetchFunc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if protocolFlag == QUIC.String() {
|
||||
return newAutoProtocolSelector(QUIC, []Protocol{QUIC, HTTP2, H2mux}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
if protocolFlag == HTTP2.String() {
|
||||
if http2Percentage < 0 {
|
||||
return newAutoProtocolSelector(H2mux, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
return newAutoProtocolSelector(HTTP2, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
return newAutoProtocolSelector(HTTP2, []Protocol{HTTP2, H2mux}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
if protocolFlag != autoSelectFlag {
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
threshold := switchThreshold(namedTunnel.Credentials.AccountTag)
|
||||
if threshold < http2Percentage {
|
||||
return newAutoProtocolSelector(HTTP2, threshold, fetchFunc, ttl, log), nil
|
||||
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUIC, HTTP2, H2mux}, threshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
func selectWarpRoutingProtocols(
|
||||
protocolFlag string,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
threshold int32,
|
||||
protocol Protocol,
|
||||
) (ProtocolSelector, error) {
|
||||
if protocolFlag == QUIC.String() {
|
||||
return newAutoProtocolSelector(QUICWarp, []Protocol{QUICWarp, HTTP2Warp}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
return newAutoProtocolSelector(H2mux, threshold, fetchFunc, ttl, log), nil
|
||||
|
||||
if protocolFlag == HTTP2.String() {
|
||||
return newAutoProtocolSelector(HTTP2Warp, []Protocol{HTTP2Warp}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
if protocolFlag != autoSelectFlag {
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUICWarp, HTTP2Warp}, threshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
func switchThreshold(accountTag string) int32 {
|
||||
|
||||
+108
-62
@@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,29 +23,23 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
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")
|
||||
func mockFetcher(getError bool, protocolPercent ...edgediscovery.ProtocolPercent) PercentageFetcher {
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
if getError {
|
||||
return nil, fmt.Errorf("failed to fetch precentage")
|
||||
}
|
||||
return protocolPercent, nil
|
||||
}
|
||||
}
|
||||
|
||||
type dynamicMockFetcher struct {
|
||||
percentage int32
|
||||
err error
|
||||
protocolPercents edgediscovery.ProtocolPercents
|
||||
err error
|
||||
}
|
||||
|
||||
func (dmf *dynamicMockFetcher) fetch() PercentageFetcher {
|
||||
return func() (int32, error) {
|
||||
if dmf.err != nil {
|
||||
return 0, dmf.err
|
||||
}
|
||||
return dmf.percentage, nil
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
return dmf.protocolPercents, dmf.err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +65,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
name: "named tunnel over h2mux",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: func() (edgediscovery.ProtocolPercents, error) { return nil, nil },
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
@@ -77,28 +74,38 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(0),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel http2 disabled",
|
||||
protocol: "http2",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(-1),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel quic disabled",
|
||||
protocol: "quic",
|
||||
expectedProtocol: HTTP2,
|
||||
// Hasfallback true is because if http2 fails, then we further fallback to h2mux.
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto all http2 disabled",
|
||||
protocol: "auto",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(-1),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to h2mux",
|
||||
protocol: "auto",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(0),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
@@ -107,36 +114,71 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to quic",
|
||||
protocol: "auto",
|
||||
expectedProtocol: QUIC,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing requesting h2mux",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: HTTP2,
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing requesting h2mux picks HTTP2 even if http2 percent is -1",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing http2",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2,
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing quic",
|
||||
protocol: "quic",
|
||||
expectedProtocol: QUICWarp,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2Warp,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing auto",
|
||||
protocol: "auto",
|
||||
expectedProtocol: HTTP2,
|
||||
expectedProtocol: HTTP2Warp,
|
||||
hasFallback: false,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing auto- quic",
|
||||
protocol: "auto",
|
||||
expectedProtocol: QUICWarp,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2Warp,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
@@ -149,32 +191,35 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
{
|
||||
name: "named tunnel unknown protocol",
|
||||
protocol: "unknown",
|
||||
fetchFunc: mockFetcher(100),
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "named tunnel fetch error",
|
||||
protocol: "unknown",
|
||||
fetchFunc: mockFetcherWithError(),
|
||||
protocol: "auto",
|
||||
fetchFunc: mockFetcher(true),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
wantErr: true,
|
||||
expectedProtocol: HTTP2,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
selector, err := NewProtocolSelector(test.protocol, test.warpRoutingEnabled, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
|
||||
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))
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
selector, err := NewProtocolSelector(test.protocol, test.warpRoutingEnabled, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,27 +229,27 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}}
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", 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.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}}
|
||||
fetcher.err = nil
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}}
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}}
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
}
|
||||
|
||||
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
@@ -213,35 +258,36 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", 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.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}}
|
||||
fetcher.err = nil
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}}
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = 100
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.percentage = -1
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}}
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
}
|
||||
|
||||
func TestProtocolSelectorRefreshTTL(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{percentage: 100}
|
||||
fetcher := dynamicMockFetcher{}
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}}
|
||||
selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), time.Hour, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
|
||||
fetcher.percentage = 0
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 0}}
|
||||
assert.Equal(t, QUIC, selector.Current())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
// HTTPHeaderKey is used to get or set http headers in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPHeaderKey = "HttpHeader"
|
||||
// HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPMethodKey = "HttpMethod"
|
||||
// HTTPHostKey is used to get or set http Method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPHostKey = "HttpHost"
|
||||
)
|
||||
|
||||
// QUICConnection represents the type that facilitates Proxying via QUIC streams.
|
||||
type QUICConnection struct {
|
||||
session quic.Session
|
||||
logger *zerolog.Logger
|
||||
httpProxy OriginProxy
|
||||
gracefulShutdownC <-chan struct{}
|
||||
stoppedGracefully bool
|
||||
}
|
||||
|
||||
// NewQUICConnection returns a new instance of QUICConnection.
|
||||
func NewQUICConnection(
|
||||
ctx context.Context,
|
||||
quicConfig *quic.Config,
|
||||
edgeAddr net.Addr,
|
||||
tlsConfig *tls.Config,
|
||||
httpProxy OriginProxy,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler ControlStreamHandler,
|
||||
observer *Observer,
|
||||
) (*QUICConnection, error) {
|
||||
session, err := quic.DialAddr(edgeAddr.String(), tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to dial to edge")
|
||||
}
|
||||
|
||||
registrationStream, err := session.OpenStream()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open a registration stream")
|
||||
}
|
||||
|
||||
err = controlStreamHandler.ServeControlStream(ctx, registrationStream, connOptions, false)
|
||||
if err != nil {
|
||||
// Not wrapping error here to be consistent with the http2 message.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &QUICConnection{
|
||||
session: session,
|
||||
httpProxy: httpProxy,
|
||||
logger: observer.log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Serve starts a QUIC session that begins accepting streams.
|
||||
func (q *QUICConnection) Serve(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
stream, err := q.session.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
// context.Canceled is usually a user ctrl+c. We don't want to log an error here as it's intentional.
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "failed to accept QUIC stream")
|
||||
}
|
||||
go func() {
|
||||
defer stream.Close()
|
||||
if err = q.handleStream(stream); err != nil {
|
||||
q.logger.Err(err).Msg("Failed to handle QUIC stream")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the session with no errors specified.
|
||||
func (q *QUICConnection) Close() {
|
||||
q.session.CloseWithError(0, "")
|
||||
}
|
||||
|
||||
func (q *QUICConnection) handleStream(stream quic.Stream) error {
|
||||
connectRequest, err := quicpogs.ReadConnectRequestData(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch connectRequest.Type {
|
||||
case quicpogs.ConnectionTypeHTTP, quicpogs.ConnectionTypeWebsocket:
|
||||
req, err := buildHTTPRequest(connectRequest, stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := newHTTPResponseAdapter(stream)
|
||||
return q.httpProxy.ProxyHTTP(w, req, connectRequest.Type == quicpogs.ConnectionTypeWebsocket)
|
||||
case quicpogs.ConnectionTypeTCP:
|
||||
rwa := &streamReadWriteAcker{
|
||||
ReadWriter: stream,
|
||||
}
|
||||
return q.httpProxy.ProxyTCP(context.Background(), rwa, &TCPRequest{Dest: connectRequest.Dest})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to
|
||||
// the client.
|
||||
type streamReadWriteAcker struct {
|
||||
io.ReadWriter
|
||||
}
|
||||
|
||||
// AckConnection acks response back to the proxy.
|
||||
func (s *streamReadWriteAcker) AckConnection() error {
|
||||
return quicpogs.WriteConnectResponseData(s, nil)
|
||||
}
|
||||
|
||||
// httpResponseAdapter translates responses written by the HTTP Proxy into ones that can be used in QUIC.
|
||||
type httpResponseAdapter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func newHTTPResponseAdapter(w io.Writer) httpResponseAdapter {
|
||||
return httpResponseAdapter{w}
|
||||
}
|
||||
|
||||
func (hrw httpResponseAdapter) WriteRespHeaders(status int, header http.Header) error {
|
||||
metadata := make([]quicpogs.Metadata, 0)
|
||||
metadata = append(metadata, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(status)})
|
||||
for k, vv := range header {
|
||||
for _, v := range vv {
|
||||
httpHeaderKey := fmt.Sprintf("%s:%s", HTTPHeaderKey, k)
|
||||
metadata = append(metadata, quicpogs.Metadata{Key: httpHeaderKey, Val: v})
|
||||
}
|
||||
}
|
||||
return quicpogs.WriteConnectResponseData(hrw, nil, metadata...)
|
||||
}
|
||||
|
||||
func (hrw httpResponseAdapter) WriteErrorResponse(err error) {
|
||||
quicpogs.WriteConnectResponseData(hrw, err, quicpogs.Metadata{Key: "HttpStatus", Val: strconv.Itoa(http.StatusBadGateway)})
|
||||
}
|
||||
|
||||
func buildHTTPRequest(connectRequest *quicpogs.ConnectRequest, body io.ReadCloser) (*http.Request, error) {
|
||||
metadata := connectRequest.MetadataMap()
|
||||
dest := connectRequest.Dest
|
||||
method := metadata[HTTPMethodKey]
|
||||
host := metadata[HTTPHostKey]
|
||||
isWebsocket := connectRequest.Type == quicpogs.ConnectionTypeWebsocket
|
||||
|
||||
req, err := http.NewRequest(method, dest, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Host = host
|
||||
for _, metadata := range connectRequest.Metadata {
|
||||
if strings.Contains(metadata.Key, HTTPHeaderKey) {
|
||||
// metadata.Key is off the format httpHeaderKey:<HTTPHeader>
|
||||
httpHeaderKey := strings.Split(metadata.Key, ":")
|
||||
if len(httpHeaderKey) != 2 {
|
||||
return nil, fmt.Errorf("Header Key: %s malformed", metadata.Key)
|
||||
}
|
||||
req.Header.Add(httpHeaderKey[1], metadata.Val)
|
||||
}
|
||||
}
|
||||
// Go's http.Client automatically sends chunked request body if this value is not set on the
|
||||
// *http.Request struct regardless of header:
|
||||
// https://go.googlesource.com/go/+/go1.8rc2/src/net/http/transfer.go#154.
|
||||
if err := setContentLength(req); err != nil {
|
||||
return nil, fmt.Errorf("Error setting content-length: %w", err)
|
||||
}
|
||||
|
||||
// Go's client defaults to chunked encoding after a 200ms delay if the following cases are true:
|
||||
// * the request body blocks
|
||||
// * the content length is not set (or set to -1)
|
||||
// * the method doesn't usually have a body (GET, HEAD, DELETE, ...)
|
||||
// * there is no transfer-encoding=chunked already set.
|
||||
// So, if transfer cannot be chunked and content length is 0, we dont set a request body.
|
||||
if !isWebsocket && !isTransferEncodingChunked(req) && req.ContentLength == 0 {
|
||||
req.Body = nil
|
||||
}
|
||||
stripWebsocketUpgradeHeader(req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
func setContentLength(req *http.Request) error {
|
||||
var err error
|
||||
if contentLengthStr := req.Header.Get("Content-Length"); contentLengthStr != "" {
|
||||
req.ContentLength, err = strconv.ParseInt(contentLengthStr, 10, 64)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func isTransferEncodingChunked(req *http.Request) bool {
|
||||
transferEncodingVal := req.Header.Get("Transfer-Encoding")
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding suggests that this can be a comma
|
||||
// separated value as well.
|
||||
return strings.Contains(strings.ToLower(transferEncodingVal), "chunked")
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
// TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol.
|
||||
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
|
||||
func TestQUICServer(t *testing.T) {
|
||||
quicConfig := &quic.Config{
|
||||
KeepAlive: true,
|
||||
}
|
||||
|
||||
// Setup test.
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
// Start a UDP Listener for QUIC.
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
|
||||
// Create a simple tls config.
|
||||
tlsConfig := generateTLSConfig()
|
||||
|
||||
// Create a client config
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
|
||||
// Start a mock httpProxy
|
||||
originProxy := &mockOriginProxyWithRequest{}
|
||||
|
||||
// This is simply a sample websocket frame message.
|
||||
wsBuf := &bytes.Buffer{}
|
||||
wsutil.WriteClientBinary(wsBuf, []byte("Hello"))
|
||||
|
||||
var tests = []struct {
|
||||
desc string
|
||||
dest string
|
||||
connectionType quicpogs.ConnectionType
|
||||
metadata []quicpogs.Metadata
|
||||
message []byte
|
||||
expectedResponse []byte
|
||||
}{
|
||||
{
|
||||
desc: "test http proxy",
|
||||
dest: "/ok",
|
||||
connectionType: quicpogs.ConnectionTypeHTTP,
|
||||
metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Ray",
|
||||
Val: "123123123",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "GET",
|
||||
},
|
||||
},
|
||||
expectedResponse: []byte("OK"),
|
||||
},
|
||||
{
|
||||
desc: "test http body request streaming",
|
||||
dest: "/echo_body",
|
||||
connectionType: quicpogs.ConnectionTypeHTTP,
|
||||
metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Ray",
|
||||
Val: "123123123",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "POST",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Content-Length",
|
||||
Val: "24",
|
||||
},
|
||||
},
|
||||
message: []byte("This is the message body"),
|
||||
expectedResponse: []byte("This is the message body"),
|
||||
},
|
||||
{
|
||||
desc: "test ws proxy",
|
||||
dest: "/ws/echo",
|
||||
connectionType: quicpogs.ConnectionTypeWebsocket,
|
||||
metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
||||
Val: "Websocket",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
message: wsBuf.Bytes(),
|
||||
expectedResponse: []byte{0x82, 0x5, 0x48, 0x65, 0x6c, 0x6c, 0x6f},
|
||||
},
|
||||
{
|
||||
desc: "test tcp proxy",
|
||||
connectionType: quicpogs.ConnectionTypeTCP,
|
||||
metadata: []quicpogs.Metadata{},
|
||||
message: []byte("Here is some tcp data"),
|
||||
expectedResponse: []byte("Here is some tcp data"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
quicServer(
|
||||
t, udpListener, tlsConfig, quicConfig,
|
||||
test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse,
|
||||
)
|
||||
}()
|
||||
|
||||
controlStream := fakeControlStream{}
|
||||
|
||||
qC, err := NewQUICConnection(
|
||||
ctx,
|
||||
quicConfig,
|
||||
udpListener.LocalAddr(),
|
||||
tlsClientConfig,
|
||||
originProxy,
|
||||
&pogs.ConnectionOptions{},
|
||||
controlStream,
|
||||
NewObserver(&log, &log, false),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
go qC.Serve(ctx)
|
||||
|
||||
wg.Wait()
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeControlStream struct {
|
||||
ControlStreamHandler
|
||||
}
|
||||
|
||||
func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, shouldWaitForUnregister bool) error {
|
||||
return nil
|
||||
}
|
||||
func (fakeControlStream) IsStopped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func quicServer(
|
||||
t *testing.T,
|
||||
conn net.PacketConn,
|
||||
tlsConf *tls.Config,
|
||||
config *quic.Config,
|
||||
dest string,
|
||||
connectionType quicpogs.ConnectionType,
|
||||
metadata []quicpogs.Metadata,
|
||||
message []byte,
|
||||
expectedResponse []byte,
|
||||
) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
earlyListener, err := quic.Listen(conn, tlsConf, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
session, err := earlyListener.Accept(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
stream, err := session.OpenStreamSync(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = quicpogs.WriteConnectRequestData(stream, dest, connectionType, metadata...)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = quicpogs.ReadConnectResponseData(stream)
|
||||
require.NoError(t, err)
|
||||
|
||||
if message != nil {
|
||||
// ALPN successful. Write data.
|
||||
_, err := stream.Write([]byte(message))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
response := make([]byte, len(expectedResponse))
|
||||
stream.Read(response)
|
||||
require.NoError(t, err)
|
||||
|
||||
// For now it is an echo server. Verify if the same data is returned.
|
||||
assert.Equal(t, expectedResponse, response)
|
||||
}
|
||||
|
||||
// Setup a bare-bones TLS config for the server
|
||||
func generateTLSConfig() *tls.Config {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
template := x509.Certificate{SerialNumber: big.NewInt(1)}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
}
|
||||
|
||||
type mockOriginProxyWithRequest struct{}
|
||||
|
||||
func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Request, isWebsocket bool) error {
|
||||
// These are a series of crude tests to ensure the headers and http related data is transferred from
|
||||
// metadata.
|
||||
if r.Method == "" {
|
||||
return errors.New("method not sent")
|
||||
}
|
||||
if r.Host == "" {
|
||||
return errors.New("host not sent")
|
||||
}
|
||||
if len(r.Header) == 0 {
|
||||
return errors.New("headers not set")
|
||||
}
|
||||
|
||||
if isWebsocket {
|
||||
return wsEchoEndpoint(w, r)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/ok":
|
||||
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
||||
case "/echo_body":
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
io.Copy(w, r.Body)
|
||||
case "/error":
|
||||
return fmt.Errorf("Failed to proxy to origin")
|
||||
default:
|
||||
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBuildHTTPRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
connectRequest *quicpogs.ConnectRequest
|
||||
body io.ReadCloser
|
||||
req *http.Request
|
||||
}{
|
||||
{
|
||||
name: "check if http.Request is built correctly with content length",
|
||||
connectRequest: &quicpogs.ConnectRequest{
|
||||
Dest: "http://test.com",
|
||||
Metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
||||
Val: "Websocket",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Content-Length",
|
||||
Val: "514",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
Method: "get",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "test.com",
|
||||
},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Another-Header": []string{"Misc"},
|
||||
"Content-Length": []string{"514"},
|
||||
},
|
||||
ContentLength: 514,
|
||||
Host: "cf.host",
|
||||
Body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
{
|
||||
name: "if content length isn't part of request headers, then it's not set",
|
||||
connectRequest: &quicpogs.ConnectRequest{
|
||||
Dest: "http://test.com",
|
||||
Metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Cf-Cloudflared-Proxy-Connection-Upgrade",
|
||||
Val: "Websocket",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
Method: "get",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "test.com",
|
||||
},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Another-Header": []string{"Misc"},
|
||||
},
|
||||
ContentLength: 0,
|
||||
Host: "cf.host",
|
||||
Body: nil,
|
||||
},
|
||||
body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
{
|
||||
name: "if content length is 0, but transfer-encoding is chunked, body is not nil",
|
||||
connectRequest: &quicpogs.ConnectRequest{
|
||||
Dest: "http://test.com",
|
||||
Metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Transfer-Encoding",
|
||||
Val: "chunked",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
Method: "get",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "test.com",
|
||||
},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Another-Header": []string{"Misc"},
|
||||
"Transfer-Encoding": []string{"chunked"},
|
||||
},
|
||||
ContentLength: 0,
|
||||
Host: "cf.host",
|
||||
Body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
{
|
||||
name: "if content length is 0, but transfer-encoding is gzip,chunked, body is not nil",
|
||||
connectRequest: &quicpogs.ConnectRequest{
|
||||
Dest: "http://test.com",
|
||||
Metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Transfer-Encoding",
|
||||
Val: "gzip,chunked",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
Method: "get",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "test.com",
|
||||
},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Another-Header": []string{"Misc"},
|
||||
"Transfer-Encoding": []string{"gzip,chunked"},
|
||||
},
|
||||
ContentLength: 0,
|
||||
Host: "cf.host",
|
||||
Body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
{
|
||||
name: "if content length is 0, and connect request is a websocket, body is not nil",
|
||||
connectRequest: &quicpogs.ConnectRequest{
|
||||
Type: quicpogs.ConnectionTypeWebsocket,
|
||||
Dest: "http://test.com",
|
||||
Metadata: []quicpogs.Metadata{
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHeader:Another-Header",
|
||||
Val: "Misc",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpHost",
|
||||
Val: "cf.host",
|
||||
},
|
||||
quicpogs.Metadata{
|
||||
Key: "HttpMethod",
|
||||
Val: "get",
|
||||
},
|
||||
},
|
||||
},
|
||||
req: &http.Request{
|
||||
Method: "get",
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "test.com",
|
||||
},
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{
|
||||
"Another-Header": []string{"Misc"},
|
||||
},
|
||||
ContentLength: 0,
|
||||
Host: "cf.host",
|
||||
Body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req, err := buildHTTPRequest(test.connectRequest, test.body)
|
||||
assert.NoError(t, err)
|
||||
test.req = test.req.WithContext(req.Context())
|
||||
assert.Equal(t, test.req, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (moc *mockOriginProxyWithRequest) ProxyTCP(ctx context.Context, rwa ReadWriteAcker, tcpRequest *TCPRequest) error {
|
||||
rwa.AckConnection()
|
||||
io.Copy(rwa, rwa)
|
||||
return nil
|
||||
}
|
||||
@@ -159,8 +159,6 @@ func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter C
|
||||
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)
|
||||
}
|
||||
@@ -187,14 +185,6 @@ func (h *h2muxConnection) processRegistrationSuccess(
|
||||
h.observer.log.Info().Msgf("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()
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ const (
|
||||
dotServerName = "cloudflare-dns.com"
|
||||
dotServerAddr = "1.1.1.1:853"
|
||||
dotTimeout = 15 * time.Second
|
||||
|
||||
logFieldAddress = "address"
|
||||
)
|
||||
|
||||
// Redeclare network functions so they can be overridden in tests.
|
||||
@@ -30,6 +32,12 @@ var (
|
||||
netLookupIP = net.LookupIP
|
||||
)
|
||||
|
||||
// EdgeAddr is a representation of possible ways to refer an edge location.
|
||||
type EdgeAddr struct {
|
||||
TCP *net.TCPAddr
|
||||
UDP *net.UDPAddr
|
||||
}
|
||||
|
||||
// If the call to net.LookupSRV fails, try to fall back to DoT from Cloudflare directly.
|
||||
//
|
||||
// Note: Instead of DoT, we could also have used DoH. Either of these:
|
||||
@@ -53,7 +61,9 @@ var friendlyDNSErrorLines = []string{
|
||||
}
|
||||
|
||||
// EdgeDiscovery implements HA service discovery lookup.
|
||||
func edgeDiscovery(log *zerolog.Logger) ([][]*net.TCPAddr, error) {
|
||||
func edgeDiscovery(log *zerolog.Logger, srvService string) ([][]*EdgeAddr, error) {
|
||||
log.Debug().Str("domain", "_"+srvService+"._"+srvProto+"."+srvName).Msg("looking up edge SRV record")
|
||||
|
||||
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
|
||||
if err != nil {
|
||||
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
|
||||
@@ -69,19 +79,19 @@ func edgeDiscovery(log *zerolog.Logger) ([][]*net.TCPAddr, error) {
|
||||
addrs = fallbackAddrs
|
||||
}
|
||||
|
||||
var resolvedIPsPerCNAME [][]*net.TCPAddr
|
||||
var resolvedAddrPerCNAME [][]*EdgeAddr
|
||||
for _, addr := range addrs {
|
||||
ips, err := resolveSRVToTCP(addr)
|
||||
edgeAddrs, err := resolveSRV(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips)
|
||||
resolvedAddrPerCNAME = append(resolvedAddrPerCNAME, edgeAddrs)
|
||||
}
|
||||
|
||||
return resolvedIPsPerCNAME, nil
|
||||
return resolvedAddrPerCNAME, nil
|
||||
}
|
||||
|
||||
func lookupSRVWithDOT(service, proto, name string) (cname string, addrs []*net.SRV, err error) {
|
||||
func lookupSRVWithDOT(srvService string, srvProto string, srvName string) (cname string, addrs []*net.SRV, err error) {
|
||||
// Inspiration: https://github.com/artyom/dot/blob/master/dot.go
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
@@ -100,7 +110,7 @@ func lookupSRVWithDOT(service, proto, name string) (cname string, addrs []*net.S
|
||||
return r.LookupSRV(ctx, srvService, srvProto, srvName)
|
||||
}
|
||||
|
||||
func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
func resolveSRV(srv *net.SRV) ([]*EdgeAddr, error) {
|
||||
ips, err := netLookupIP(srv.Target)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't resolve SRV record %v", srv)
|
||||
@@ -108,23 +118,36 @@ func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) {
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("SRV record %v had no IPs", srv)
|
||||
}
|
||||
addrs := make([]*net.TCPAddr, len(ips))
|
||||
addrs := make([]*EdgeAddr, len(ips))
|
||||
for i, ip := range ips {
|
||||
addrs[i] = &net.TCPAddr{IP: ip, Port: int(srv.Port)}
|
||||
addrs[i] = &EdgeAddr{
|
||||
TCP: &net.TCPAddr{IP: ip, Port: int(srv.Port)},
|
||||
UDP: &net.UDPAddr{IP: ip, Port: int(srv.Port)},
|
||||
}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// 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, log *zerolog.Logger) (resolved []*net.TCPAddr) {
|
||||
func ResolveAddrs(addrs []string, log *zerolog.Logger) (resolved []*EdgeAddr) {
|
||||
for _, addr := range addrs {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Failed to resolve %s", addr)
|
||||
} else {
|
||||
resolved = append(resolved, tcpAddr)
|
||||
log.Error().Str(logFieldAddress, addr).Err(err).Msg("failed to resolve to TCP address")
|
||||
continue
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
log.Error().Str(logFieldAddress, addr).Err(err).Msg("failed to resolve to UDP address")
|
||||
continue
|
||||
}
|
||||
resolved = append(resolved, &EdgeAddr{
|
||||
TCP: tcpAddr,
|
||||
UDP: udpAddr,
|
||||
})
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func (ea *EdgeAddr) String() string {
|
||||
return fmt.Sprintf("%s-%s", ea.TCP, ea.UDP)
|
||||
}
|
||||
|
||||
func TestEdgeDiscovery(t *testing.T) {
|
||||
mockAddrs := newMockAddrs(19, 2, 5)
|
||||
netLookupSRV = mockNetLookupSRV(mockAddrs)
|
||||
@@ -20,7 +25,7 @@ func TestEdgeDiscovery(t *testing.T) {
|
||||
}
|
||||
|
||||
l := zerolog.Nop()
|
||||
addrLists, err := edgeDiscovery(&l)
|
||||
addrLists, err := edgeDiscovery(&l, "")
|
||||
assert.NoError(t, err)
|
||||
actualAddrSet := map[string]bool{}
|
||||
for _, addrs := range addrLists {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
type mockAddrs struct {
|
||||
// a set of synthetic SRV records
|
||||
addrMap map[net.SRV][]*net.TCPAddr
|
||||
addrMap map[net.SRV][]*EdgeAddr
|
||||
// the total number of addresses, aggregated across addrMap.
|
||||
// For the convenience of test code that would otherwise have to compute
|
||||
// this by hand every time.
|
||||
@@ -19,19 +19,24 @@ type mockAddrs struct {
|
||||
}
|
||||
|
||||
func newMockAddrs(port uint16, numRegions uint8, numAddrsPerRegion uint8) mockAddrs {
|
||||
addrMap := make(map[net.SRV][]*net.TCPAddr)
|
||||
addrMap := make(map[net.SRV][]*EdgeAddr)
|
||||
numAddrs := 0
|
||||
|
||||
for r := uint8(0); r < numRegions; r++ {
|
||||
var (
|
||||
srv = net.SRV{Target: fmt.Sprintf("test-region-%v.example.com", r), Port: port}
|
||||
addrs []*net.TCPAddr
|
||||
addrs []*EdgeAddr
|
||||
)
|
||||
for a := uint8(0); a < numAddrsPerRegion; a++ {
|
||||
addrs = append(addrs, &net.TCPAddr{
|
||||
tcpAddr := &net.TCPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
})
|
||||
}
|
||||
udpAddr := &net.UDPAddr{
|
||||
IP: net.ParseIP(fmt.Sprintf("10.0.%v.%v", r, a)),
|
||||
Port: int(port),
|
||||
}
|
||||
addrs = append(addrs, &EdgeAddr{tcpAddr, udpAddr})
|
||||
}
|
||||
addrMap[srv] = addrs
|
||||
numAddrs += len(addrs)
|
||||
@@ -74,13 +79,13 @@ func mockNetLookupIP(
|
||||
m mockAddrs,
|
||||
) func(host string) ([]net.IP, error) {
|
||||
return func(host string) ([]net.IP, error) {
|
||||
for srv, tcpAddrs := range m.addrMap {
|
||||
for srv, addrs := range m.addrMap {
|
||||
if srv.Target != host {
|
||||
continue
|
||||
}
|
||||
result := make([]net.IP, len(tcpAddrs))
|
||||
for i, tcpAddr := range tcpAddrs {
|
||||
result[i] = tcpAddr.IP
|
||||
result := make([]net.IP, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
result[i] = addr.TCP.IP
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Region contains cloudflared edge addresses. The edge is partitioned into several regions for
|
||||
// redundancy purposes.
|
||||
type Region struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
|
||||
// NewRegion creates a region with the given addresses, which are all unused.
|
||||
func NewRegion(addrs []*net.TCPAddr) Region {
|
||||
func NewRegion(addrs []*EdgeAddr) Region {
|
||||
// The zero value of UsedBy is Unused(), so we can just initialize the map's values with their
|
||||
// zero values.
|
||||
m := make(map[*net.TCPAddr]UsedBy)
|
||||
connFor := make(map[*EdgeAddr]UsedBy)
|
||||
for _, addr := range addrs {
|
||||
m[addr] = Unused()
|
||||
connFor[addr] = Unused()
|
||||
}
|
||||
return Region{
|
||||
connFor: connFor,
|
||||
}
|
||||
return Region{connFor: m}
|
||||
}
|
||||
|
||||
// AddrUsedBy finds the address used by the given connection in this region.
|
||||
// Returns nil if the connection isn't using any IP.
|
||||
func (r *Region) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
func (r *Region) AddrUsedBy(connID int) *EdgeAddr {
|
||||
for addr, used := range r.connFor {
|
||||
if used.Used && used.ConnID == connID {
|
||||
return addr
|
||||
@@ -45,7 +43,7 @@ func (r Region) AvailableAddrs() int {
|
||||
|
||||
// GetUnusedIP returns a random unused address in this region.
|
||||
// Returns nil if all addresses are in use.
|
||||
func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
|
||||
func (r Region) GetUnusedIP(excluding *EdgeAddr) *EdgeAddr {
|
||||
for addr, usedby := range r.connFor {
|
||||
if !usedby.Used && addr != excluding {
|
||||
return addr
|
||||
@@ -55,7 +53,7 @@ func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
|
||||
}
|
||||
|
||||
// Use the address, assigning it to a proxy connection.
|
||||
func (r Region) Use(addr *net.TCPAddr, connID int) {
|
||||
func (r Region) Use(addr *EdgeAddr, connID int) {
|
||||
if addr == nil {
|
||||
return
|
||||
}
|
||||
@@ -63,7 +61,7 @@ func (r Region) Use(addr *net.TCPAddr, connID int) {
|
||||
}
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the region.
|
||||
func (r Region) GetAnyAddress() *net.TCPAddr {
|
||||
func (r Region) GetAnyAddress() *EdgeAddr {
|
||||
for addr := range r.connFor {
|
||||
return addr
|
||||
}
|
||||
@@ -72,7 +70,7 @@ func (r Region) GetAnyAddress() *net.TCPAddr {
|
||||
|
||||
// GiveBack the address, ensuring it is no longer assigned to an IP.
|
||||
// Returns true if the address is in this region.
|
||||
func (r Region) GiveBack(addr *net.TCPAddr) (ok bool) {
|
||||
func (r Region) GiveBack(addr *EdgeAddr) (ok bool) {
|
||||
if _, ok := r.connFor[addr]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegion_New(t *testing.T) {
|
||||
r := NewRegion([]*net.TCPAddr{&addr0, &addr1, &addr2})
|
||||
fmt.Println(r.connFor)
|
||||
r := NewRegion([]*EdgeAddr{&addr0, &addr1, &addr2})
|
||||
if r.AvailableAddrs() != 3 {
|
||||
t.Errorf("r.AvailableAddrs() == %v but want 3", r.AvailableAddrs())
|
||||
}
|
||||
@@ -17,7 +14,7 @@ func TestRegion_New(t *testing.T) {
|
||||
|
||||
func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
connID int
|
||||
@@ -26,11 +23,11 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *net.TCPAddr
|
||||
want *EdgeAddr
|
||||
}{
|
||||
{
|
||||
name: "happy trivial test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
}},
|
||||
args: args{connID: 0},
|
||||
@@ -38,7 +35,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad trivial test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
}},
|
||||
args: args{connID: 1},
|
||||
@@ -46,7 +43,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -56,7 +53,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "happy test",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -79,7 +76,7 @@ func TestRegion_AddrUsedBy(t *testing.T) {
|
||||
|
||||
func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -88,7 +85,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "contains addresses",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
@@ -97,7 +94,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "all free",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: Unused(),
|
||||
&addr2: Unused(),
|
||||
@@ -106,7 +103,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "all used",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -115,7 +112,7 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{}},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
@@ -133,20 +130,20 @@ func TestRegion_AvailableAddrs(t *testing.T) {
|
||||
|
||||
func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
excluding *net.TCPAddr
|
||||
excluding *EdgeAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *net.TCPAddr
|
||||
want *EdgeAddr
|
||||
}{
|
||||
{
|
||||
name: "happy test with excluding set",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
@@ -156,7 +153,7 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "happy test with no excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: Unused(),
|
||||
&addr2: InUse(2),
|
||||
@@ -166,7 +163,7 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test with no excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(0),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -176,7 +173,7 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
&addr1: InUse(1),
|
||||
&addr2: InUse(2),
|
||||
@@ -199,10 +196,10 @@ func TestRegion_GetUnusedIP(t *testing.T) {
|
||||
|
||||
func TestRegion_GiveBack(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
type args struct {
|
||||
addr *net.TCPAddr
|
||||
addr *EdgeAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -213,7 +210,7 @@ func TestRegion_GiveBack(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr1: InUse(1),
|
||||
}},
|
||||
args: args{addr: &addr1},
|
||||
@@ -222,7 +219,7 @@ func TestRegion_GiveBack(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "sad test with excluding",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr1: InUse(1),
|
||||
}},
|
||||
args: args{addr: &addr2},
|
||||
@@ -247,7 +244,7 @@ func TestRegion_GiveBack(t *testing.T) {
|
||||
|
||||
func TestRegion_GetAnyAddress(t *testing.T) {
|
||||
type fields struct {
|
||||
connFor map[*net.TCPAddr]UsedBy
|
||||
connFor map[*EdgeAddr]UsedBy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -256,19 +253,19 @@ func TestRegion_GetAnyAddress(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "Sad test -- GetAnyAddress should only fail if the region is empty",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{}},
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{}},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "Happy test (all addresses unused)",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: Unused(),
|
||||
}},
|
||||
wantNil: false,
|
||||
},
|
||||
{
|
||||
name: "Happy test (GetAnyAddress can still return addresses used by proxy conns)",
|
||||
fields: fields{connFor: map[*net.TCPAddr]UsedBy{
|
||||
fields: fields{connFor: map[*EdgeAddr]UsedBy{
|
||||
&addr0: InUse(2),
|
||||
}},
|
||||
wantNil: false,
|
||||
|
||||
@@ -2,7 +2,6 @@ package allregions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -19,17 +18,17 @@ type Regions struct {
|
||||
// ------------------------------------
|
||||
|
||||
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
|
||||
func ResolveEdge(log *zerolog.Logger) (*Regions, error) {
|
||||
addrLists, err := edgeDiscovery(log)
|
||||
func ResolveEdge(log *zerolog.Logger, region string) (*Regions, error) {
|
||||
edgeAddrs, err := edgeDiscovery(log, getRegionalServiceName(region))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(addrLists) < 2 {
|
||||
return nil, fmt.Errorf("expected at least 2 Cloudflare Regions regions, but SRV only returned %v", len(addrLists))
|
||||
if len(edgeAddrs) < 2 {
|
||||
return nil, fmt.Errorf("expected at least 2 Cloudflare Regions regions, but SRV only returned %v", len(edgeAddrs))
|
||||
}
|
||||
return &Regions{
|
||||
region1: NewRegion(addrLists[0]),
|
||||
region2: NewRegion(addrLists[1]),
|
||||
region1: NewRegion(edgeAddrs[0]),
|
||||
region2: NewRegion(edgeAddrs[1]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -45,9 +44,9 @@ func StaticEdge(hostnames []string, log *zerolog.Logger) (*Regions, error) {
|
||||
|
||||
// NewNoResolve doesn't resolve the edge. Instead it just uses the given addresses.
|
||||
// You probably only need this for testing.
|
||||
func NewNoResolve(addrs []*net.TCPAddr) *Regions {
|
||||
region1 := make([]*net.TCPAddr, 0)
|
||||
region2 := make([]*net.TCPAddr, 0)
|
||||
func NewNoResolve(addrs []*EdgeAddr) *Regions {
|
||||
region1 := make([]*EdgeAddr, 0)
|
||||
region2 := make([]*EdgeAddr, 0)
|
||||
for i, v := range addrs {
|
||||
if i%2 == 0 {
|
||||
region1 = append(region1, v)
|
||||
@@ -67,7 +66,7 @@ func NewNoResolve(addrs []*net.TCPAddr) *Regions {
|
||||
// ------------------------------------
|
||||
|
||||
// GetAnyAddress returns an arbitrary address from the larger region.
|
||||
func (rs *Regions) GetAnyAddress() *net.TCPAddr {
|
||||
func (rs *Regions) GetAnyAddress() *EdgeAddr {
|
||||
if addr := rs.region1.GetAnyAddress(); addr != nil {
|
||||
return addr
|
||||
}
|
||||
@@ -76,7 +75,7 @@ func (rs *Regions) GetAnyAddress() *net.TCPAddr {
|
||||
|
||||
// AddrUsedBy finds the address used by the given connection.
|
||||
// Returns nil if the connection isn't using an address.
|
||||
func (rs *Regions) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
func (rs *Regions) AddrUsedBy(connID int) *EdgeAddr {
|
||||
if addr := rs.region1.AddrUsedBy(connID); addr != nil {
|
||||
return addr
|
||||
}
|
||||
@@ -85,7 +84,7 @@ func (rs *Regions) AddrUsedBy(connID int) *net.TCPAddr {
|
||||
|
||||
// GetUnusedAddr gets an unused addr from the edge, excluding the given addr. Prefer to use addresses
|
||||
// evenly across both regions.
|
||||
func (rs *Regions) GetUnusedAddr(excluding *net.TCPAddr, connID int) *net.TCPAddr {
|
||||
func (rs *Regions) GetUnusedAddr(excluding *EdgeAddr, connID int) *EdgeAddr {
|
||||
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
|
||||
return getAddrs(excluding, connID, &rs.region1, &rs.region2)
|
||||
}
|
||||
@@ -95,7 +94,7 @@ func (rs *Regions) GetUnusedAddr(excluding *net.TCPAddr, connID int) *net.TCPAdd
|
||||
|
||||
// getAddrs tries to grab address form `first` region, then `second` region
|
||||
// this is an unrolled loop over 2 element array
|
||||
func getAddrs(excluding *net.TCPAddr, connID int, first *Region, second *Region) *net.TCPAddr {
|
||||
func getAddrs(excluding *EdgeAddr, connID int, first *Region, second *Region) *EdgeAddr {
|
||||
addr := first.GetUnusedIP(excluding)
|
||||
if addr != nil {
|
||||
first.Use(addr, connID)
|
||||
@@ -117,9 +116,18 @@ func (rs *Regions) AvailableAddrs() int {
|
||||
|
||||
// GiveBack the address so that other connections can use it.
|
||||
// Returns true if the address is in this edge.
|
||||
func (rs *Regions) GiveBack(addr *net.TCPAddr) bool {
|
||||
func (rs *Regions) GiveBack(addr *EdgeAddr) bool {
|
||||
if found := rs.region1.GiveBack(addr); found {
|
||||
return found
|
||||
}
|
||||
return rs.region2.GiveBack(addr)
|
||||
}
|
||||
|
||||
// Return regionalized service name if `region` isn't empty, otherwise return the global service name for origintunneld
|
||||
func getRegionalServiceName(region string) string {
|
||||
if region != "" {
|
||||
return region + "-" + srvService // Example: `us-origintunneld`
|
||||
}
|
||||
|
||||
return srvService // Global service is just `origintunneld`
|
||||
}
|
||||
|
||||
@@ -8,31 +8,59 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
addr0 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr0 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr1 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr1 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr2 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr2 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr3 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr3 = EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.4.5.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.4.5.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func makeRegions() Regions {
|
||||
r1 := NewRegion([]*net.TCPAddr{&addr0, &addr1})
|
||||
r2 := NewRegion([]*net.TCPAddr{&addr2, &addr3})
|
||||
r1 := NewRegion([]*EdgeAddr{&addr0, &addr1})
|
||||
r2 := NewRegion([]*EdgeAddr{&addr2, &addr3})
|
||||
return Regions{region1: r1, region2: r2}
|
||||
}
|
||||
|
||||
@@ -105,7 +133,7 @@ func TestRegions_GetUnusedAddr_Excluding_Region2(t *testing.T) {
|
||||
|
||||
func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
type args struct {
|
||||
addrs []*net.TCPAddr
|
||||
addrs []*EdgeAddr
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -113,11 +141,11 @@ func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "one address",
|
||||
args: args{addrs: []*net.TCPAddr{&addr0}},
|
||||
args: args{addrs: []*EdgeAddr{&addr0}},
|
||||
},
|
||||
{
|
||||
name: "two addresses",
|
||||
args: args{addrs: []*net.TCPAddr{&addr0, &addr1}},
|
||||
args: args{addrs: []*EdgeAddr{&addr0, &addr1}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -128,6 +156,18 @@ func TestNewNoResolveBalancesRegions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegionalServiceName(t *testing.T) {
|
||||
// Empty region should just go to origintunneld
|
||||
globalServiceName := getRegionalServiceName("")
|
||||
assert.Equal(t, srvService, globalServiceName)
|
||||
|
||||
// Non-empty region should go to the regional origintunneld variant
|
||||
for _, region := range []string{"us", "pt", "am"} {
|
||||
regionalServiceName := getRegionalServiceName(region)
|
||||
assert.Equal(t, region+"-"+srvService, regionalServiceName)
|
||||
}
|
||||
}
|
||||
|
||||
func RegionsIsBalanced(t *testing.T, rs *Regions) {
|
||||
delta := rs.region1.AvailableAddrs() - rs.region2.AvailableAddrs()
|
||||
assert.True(t, abs(delta) <= 1)
|
||||
|
||||
@@ -2,7 +2,6 @@ package edgediscovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
LogFieldAddress = "address"
|
||||
LogFieldConnIndex = "connIndex"
|
||||
)
|
||||
|
||||
@@ -30,8 +28,8 @@ type Edge struct {
|
||||
|
||||
// ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated
|
||||
// to connections.
|
||||
func ResolveEdge(log *zerolog.Logger) (*Edge, error) {
|
||||
regions, err := allregions.ResolveEdge(log)
|
||||
func ResolveEdge(log *zerolog.Logger, region string) (*Edge, error) {
|
||||
regions, err := allregions.ResolveEdge(log, region)
|
||||
if err != nil {
|
||||
return new(Edge), err
|
||||
}
|
||||
@@ -54,7 +52,7 @@ func StaticEdge(log *zerolog.Logger, hostnames []string) (*Edge, error) {
|
||||
}
|
||||
|
||||
// MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing.
|
||||
func MockEdge(log *zerolog.Logger, addrs []*net.TCPAddr) *Edge {
|
||||
func MockEdge(log *zerolog.Logger, addrs []*allregions.EdgeAddr) *Edge {
|
||||
regions := allregions.NewNoResolve(addrs)
|
||||
return &Edge{
|
||||
log: log,
|
||||
@@ -67,7 +65,7 @@ func MockEdge(log *zerolog.Logger, addrs []*net.TCPAddr) *Edge {
|
||||
// ------------------------------------
|
||||
|
||||
// GetAddrForRPC gives this connection an edge Addr.
|
||||
func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetAddrForRPC() (*allregions.EdgeAddr, error) {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
addr := ed.regions.GetAnyAddress()
|
||||
@@ -78,9 +76,8 @@ func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
|
||||
}
|
||||
|
||||
// GetAddr gives this proxy connection an edge Addr. Prefer Addrs this connection has already used.
|
||||
func (ed *Edge) GetAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetAddr(connIndex int) (*allregions.EdgeAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
|
||||
@@ -96,12 +93,12 @@ func (ed *Edge) GetAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
log.Debug().Msg("edgediscovery - GetAddr: No addresses left to give proxy connection")
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
log.Debug().Str(LogFieldAddress, addr.String()).Msg("edgediscovery - GetAddr: Giving connection its new address")
|
||||
log.Debug().Msg("edgediscovery - GetAddr: Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// GetDifferentAddr gives back the proxy connection's edge Addr and uses a new one.
|
||||
func (ed *Edge) GetDifferentAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
func (ed *Edge) GetDifferentAddr(connIndex int) (*allregions.EdgeAddr, error) {
|
||||
log := ed.log.With().Int(LogFieldConnIndex, connIndex).Logger()
|
||||
|
||||
ed.Lock()
|
||||
@@ -117,7 +114,7 @@ func (ed *Edge) GetDifferentAddr(connIndex int) (*net.TCPAddr, error) {
|
||||
// note: if oldAddr were not nil, it will become available on the next iteration
|
||||
return nil, errNoAddressesLeft
|
||||
}
|
||||
log.Debug().Str(LogFieldAddress, addr.String()).Msg("edgediscovery - GetDifferentAddr: Giving connection its new address")
|
||||
log.Debug().Msg("edgediscovery - GetDifferentAddr: Giving connection its new address")
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
@@ -130,7 +127,7 @@ func (ed *Edge) AvailableAddrs() int {
|
||||
|
||||
// GiveBack the address so that other connections can use it.
|
||||
// Returns true if the address is in this edge.
|
||||
func (ed *Edge) GiveBack(addr *net.TCPAddr) bool {
|
||||
func (ed *Edge) GiveBack(addr *allregions.EdgeAddr) bool {
|
||||
ed.Lock()
|
||||
defer ed.Unlock()
|
||||
ed.log.Debug().Msg("edgediscovery - GiveBack: Address now unused")
|
||||
|
||||
@@ -6,35 +6,65 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
|
||||
var (
|
||||
addr0 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr0 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.0"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr1 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr1 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.1"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr2 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr2 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.2"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
addr3 = net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
addr3 = allregions.EdgeAddr{
|
||||
TCP: &net.TCPAddr{
|
||||
IP: net.ParseIP("123.0.0.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
UDP: &net.UDPAddr{
|
||||
IP: net.ParseIP("123.0.0.3"),
|
||||
Port: 8000,
|
||||
Zone: "",
|
||||
},
|
||||
}
|
||||
|
||||
log = zerolog.Nop()
|
||||
testLogger = zerolog.Nop()
|
||||
)
|
||||
|
||||
func TestGiveBack(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
@@ -51,7 +81,7 @@ func TestGiveBack(t *testing.T) {
|
||||
|
||||
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
// Make an edge with a single IP
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0})
|
||||
tunnelConnID := 0
|
||||
|
||||
// Use the IP for a tunnel
|
||||
@@ -65,7 +95,7 @@ func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddrForRPC(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Get a connection
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
@@ -83,7 +113,7 @@ func TestGetAddrForRPC(t *testing.T) {
|
||||
|
||||
func TestOnePerRegion(t *testing.T) {
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1})
|
||||
|
||||
// Use the only address
|
||||
const connID = 0
|
||||
@@ -105,7 +135,7 @@ func TestOnePerRegion(t *testing.T) {
|
||||
|
||||
func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
// Make an edge with only one address
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0})
|
||||
|
||||
// Use the only address
|
||||
const connID = 0
|
||||
@@ -125,7 +155,7 @@ func TestOnlyOneAddrLeft(t *testing.T) {
|
||||
|
||||
func TestNoAddrsLeft(t *testing.T) {
|
||||
// Make an edge with no addresses
|
||||
edge := MockEdge(&log, []*net.TCPAddr{})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{})
|
||||
|
||||
_, err := edge.GetAddr(2)
|
||||
assert.Error(t, err)
|
||||
@@ -134,7 +164,7 @@ func TestNoAddrsLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetAddr(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
const connID = 0
|
||||
@@ -149,7 +179,7 @@ func TestGetAddr(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetDifferentAddr(t *testing.T) {
|
||||
edge := MockEdge(&log, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
edge := MockEdge(&testLogger, []*allregions.EdgeAddr{&addr0, &addr1, &addr2, &addr3})
|
||||
|
||||
// Give this connection an address
|
||||
assert.Equal(t, 4, edge.AvailableAddrs())
|
||||
|
||||
+28
-23
@@ -1,45 +1,50 @@
|
||||
package edgediscovery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
protocolRecord = "protocol.argotunnel.com"
|
||||
protocolRecord = "protocol-v2.argotunnel.com"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoProtocolRecord = fmt.Errorf("No TXT record found for %s to determine connection protocol", protocolRecord)
|
||||
)
|
||||
|
||||
func HTTP2Percentage() (int32, error) {
|
||||
// ProtocolPercent represents a single Protocol Percentage combination.
|
||||
type ProtocolPercent struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Percentage int32 `json:"percentage"`
|
||||
}
|
||||
|
||||
// ProtocolPercents represents the preferred distribution ratio of protocols when protocol isn't specified.
|
||||
type ProtocolPercents []ProtocolPercent
|
||||
|
||||
// GetPercentage returns the threshold percentage of a single protocol.
|
||||
func (p ProtocolPercents) GetPercentage(protocol string) int32 {
|
||||
for _, protocolPercent := range p {
|
||||
if strings.ToLower(protocolPercent.Protocol) == strings.ToLower(protocol) {
|
||||
return protocolPercent.Percentage
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ProtocolPercentage returns the ratio of protocols and a specification ratio for their selection.
|
||||
func ProtocolPercentage() (ProtocolPercents, error) {
|
||||
records, err := net.LookupTXT(protocolRecord)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return nil, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return 0, errNoProtocolRecord
|
||||
return nil, 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
|
||||
|
||||
var protocolsWithPercent ProtocolPercents
|
||||
err = json.Unmarshal([]byte(records[0]), &protocolsWithPercent)
|
||||
return protocolsWithPercent, err
|
||||
}
|
||||
|
||||
@@ -6,75 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHTTP2Percentage(t *testing.T) {
|
||||
_, err := HTTP2Percentage()
|
||||
func TestProtocolPercentage(t *testing.T) {
|
||||
_, err := ProtocolPercentage()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-32
@@ -10,6 +10,9 @@ import shutil
|
||||
import hashlib
|
||||
import requests
|
||||
import tarfile
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
import re
|
||||
|
||||
from github import Github, GithubException, UnknownObjectException
|
||||
|
||||
@@ -135,7 +138,7 @@ def parse_args():
|
||||
logging.error("Missing asset path")
|
||||
is_valid = False
|
||||
|
||||
if not args.name:
|
||||
if not args.name and not os.path.isdir(args.path):
|
||||
logging.error("Missing asset name")
|
||||
is_valid = False
|
||||
|
||||
@@ -161,6 +164,38 @@ def parse_args():
|
||||
parser.print_usage()
|
||||
exit(1)
|
||||
|
||||
def upload_asset(release, filepath, filename, release_version, kv_account_id, namespace_id, kv_api_token):
|
||||
logging.info("Uploading asset: %s", filename)
|
||||
release.upload_asset(filepath, name=filename)
|
||||
|
||||
# check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
|
||||
binary_path = filepath
|
||||
if binary_path.endswith("tgz"):
|
||||
try:
|
||||
shutil.rmtree('cfd')
|
||||
except OSError:
|
||||
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, filename, release_version, kv_account_id, namespace_id, 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, filename)
|
||||
try:
|
||||
shutil.copy(filepath, copy_path)
|
||||
except shutil.SameFileError:
|
||||
pass # the macOS release copy fails with being the same file (already in the artifacts directory)
|
||||
|
||||
def main():
|
||||
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
|
||||
@@ -174,40 +209,18 @@ def main():
|
||||
logging.info("Skipping asset upload because of dry-run")
|
||||
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.
|
||||
if os.path.isdir(args.path):
|
||||
onlyfiles = [f for f in listdir(args.path) if isfile(join(args.path, f))]
|
||||
for filename in onlyfiles:
|
||||
binary_path = os.path.join(args.path, filename)
|
||||
upload_asset(release, binary_path, filename, args.release_version, args.kv_account_id, args.namespace_id,
|
||||
args.kv_api_token)
|
||||
else:
|
||||
upload_asset(release, args.path, args.name, args.release_version, args.kv_account_id, args.namespace_id,
|
||||
args.kv_api_token)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
exit(1)
|
||||
|
||||
|
||||
main()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/cloudflare/cloudflared
|
||||
|
||||
go 1.15
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
@@ -22,13 +22,13 @@ require (
|
||||
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/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.2
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.23.0
|
||||
github.com/mattn/go-colorable v0.1.8
|
||||
github.com/miekg/dns v1.1.31
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
@@ -37,6 +37,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/prometheus/common v0.13.0 // indirect
|
||||
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92
|
||||
github.com/rs/zerolog v1.20.0
|
||||
@@ -44,11 +45,11 @@ require (
|
||||
github.com/stretchr/testify v1.6.0
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
go.uber.org/automaxprocs v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
|
||||
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-20210119212857-b64e53b001e4
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
|
||||
google.golang.org/grpc v1.32.0 // indirect
|
||||
@@ -56,9 +57,44 @@ require (
|
||||
gopkg.in/coreos/go-oidc.v2 v2.1.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/caddy v1.0.5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.8 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.1.3 // indirect
|
||||
github.com/rivo/uniseg v0.1.0 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/tools v0.1.1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
)
|
||||
|
||||
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
@@ -31,7 +33,12 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-sdk-for-go v40.6.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
|
||||
@@ -84,6 +91,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
@@ -101,6 +109,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/caddyserver/caddy v1.0.5 h1:5B1Hs0UF2x2tggr2X9jL2qOZtDXbIWQb9YLbmlxHSuM=
|
||||
github.com/caddyserver/caddy v1.0.5/go.mod h1:AnFHB+/MrgRC+mJAvuAgQ38ePzw+wKeW0wzENpdQQKY=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
@@ -113,6 +123,7 @@ github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjw
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
@@ -133,6 +144,7 @@ github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmq
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
@@ -185,6 +197,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -198,6 +212,7 @@ github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 h1:YO10pIIBftO/
|
||||
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-acme/lego/v3 v3.1.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
|
||||
github.com/go-acme/lego/v3 v3.2.0/go.mod h1:074uqt+JS6plx+c9Xaiz6+L+GBb+7itGtzfcDM2AhEE=
|
||||
github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
|
||||
@@ -220,6 +235,8 @@ github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dp
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58 h1:YyrUZvJaU8Q0QsoVo+xLFBgWDTam29PKea6GYmwvSiQ=
|
||||
github.com/gobwas/httphead v0.0.0-20200921212729-da3d93bc3c58/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
@@ -241,6 +258,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
@@ -248,6 +266,8 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
@@ -262,8 +282,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
@@ -274,8 +296,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -293,6 +316,8 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
@@ -312,6 +337,7 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
|
||||
@@ -350,6 +376,7 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod
|
||||
github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062/go.mod h1:PcNJqIlcX/dj3DTG/+QQnRvSgTMG6CLpRMjWcv4+J6w=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d h1:PRDnysJ9dF1vUMmEzBu6aHQeUluSQy4eWH3RsSSy/vI=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
@@ -379,6 +406,7 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
@@ -392,14 +420,24 @@ github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0U
|
||||
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
|
||||
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
|
||||
github.com/lucas-clemente/quic-go v0.13.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU=
|
||||
github.com/lucas-clemente/quic-go v0.23.0 h1:5vFnKtZ6nHDFsc/F3uuiF4T3y/AXaQdxjUqiVw26GZE=
|
||||
github.com/lucas-clemente/quic-go v0.23.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE=
|
||||
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
|
||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
||||
github.com/marten-seemann/qtls v0.4.1/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc=
|
||||
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@@ -415,6 +453,7 @@ github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4f
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/certmagic v0.8.3/go.mod h1:91uJzK5K8IWtYQqTi5R2tsxV1pCde+wdGfaRaOZi6aQ=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
@@ -451,11 +490,16 @@ github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw=
|
||||
github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ=
|
||||
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
@@ -464,9 +508,18 @@ github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
|
||||
github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
@@ -476,6 +529,7 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.3.5/go.mod h1:uVHyebswE1cCXr2A73cRM2frx5ld1RJUCJkFNZ90ZiI=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
@@ -500,6 +554,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
@@ -515,6 +570,7 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
@@ -523,6 +579,7 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.13.0 h1:vJlpe9wPgDRM1Z+7Wj3zUUjY1nr6/1jNKyl7llliccg=
|
||||
github.com/prometheus/common v0.13.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
@@ -543,8 +600,9 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
|
||||
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
|
||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4 h1:S9YlS71UNJIyS61OqGAmLXv3w5zclSidN+qwr80XxKs=
|
||||
github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -553,7 +611,30 @@ github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
@@ -563,6 +644,8 @@ github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:s
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
@@ -579,6 +662,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY=
|
||||
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
|
||||
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
@@ -588,6 +672,8 @@ github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
@@ -596,9 +682,12 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200306183522-221f0cc107cb/go.mod h1:VZB9Yx4s43MHItytoe8jcvaEFEgF2QzHDZGfQ/XQjvQ=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@@ -619,11 +708,15 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -635,10 +728,13 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -651,6 +747,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -669,12 +766,16 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -683,6 +784,7 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -707,19 +809,25 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -727,8 +835,10 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -736,12 +846,14 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -759,6 +871,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -777,13 +890,21 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -791,8 +912,9 @@ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fq
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -802,6 +924,7 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -849,11 +972,17 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -874,6 +1003,7 @@ google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSr
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
@@ -882,6 +1012,9 @@ google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuh
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@@ -913,6 +1046,8 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -942,8 +1077,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.24.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -970,6 +1107,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
|
||||
gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
@@ -980,11 +1118,13 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1010,5 +1150,7 @@ sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnM
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible h1:mwfXZniffG5mXokQGHUJWGnqIBggoPfT/CEwon9Yess=
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ=
|
||||
|
||||
+3
-2
@@ -69,12 +69,13 @@ func (r *MuxReader) run(log *zerolog.Logger) error {
|
||||
|
||||
// routine to periodically update bytesRead
|
||||
go func() {
|
||||
tickC := time.Tick(updateFreq)
|
||||
ticker := time.NewTicker(updateFreq)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-r.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
case <-ticker.C:
|
||||
r.metricsUpdater.updateInBoundBytes(r.bytesRead.Count())
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -78,12 +78,13 @@ func (w *MuxWriter) run(log *zerolog.Logger) error {
|
||||
|
||||
// routine to periodically communicate bytesWrote
|
||||
go func() {
|
||||
tickC := time.Tick(updateFreq)
|
||||
ticker := time.NewTicker(updateFreq)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-w.abortChan:
|
||||
return
|
||||
case <-tickC:
|
||||
case <-ticker.C:
|
||||
w.metricsUpdater.updateOutBoundBytes(w.bytesWrote.Count())
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -37,7 +37,7 @@ type OriginUpTime struct {
|
||||
UpTime string `json:"uptime"`
|
||||
}
|
||||
|
||||
const defaultServerName = "the Argo Tunnel test server"
|
||||
const defaultServerName = "the Cloudflare Tunnel test server"
|
||||
const indexTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -45,10 +45,10 @@ const indexTemplate = `
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
|
||||
<title>
|
||||
Argo Tunnel Connection
|
||||
Cloudflare Tunnel Connection
|
||||
</title>
|
||||
<meta name="author" content="">
|
||||
<meta name="description" content="Argo Tunnel Connection">
|
||||
<meta name="description" content="Cloudflare Tunnel Connection">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}section{display:block}h1{font-size:2em;margin:.67em 0}a{background-color:transparent;-webkit-text-decoration-skip:objects}/* 1 */::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}/* 1 */a,body,dd,div,dl,dt,h1,h4,html,p,section{box-sizing:border-box}.bt{border-top-style:solid;border-top-width:1px}.bl{border-left-style:solid;border-left-width:1px}.b--orange{border-color:#f38020}.br1{border-radius:.125rem}.bw2{border-width:.25rem}.dib{display:inline-block}.sans-serif{font-family:open sans,-apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif}.overflow-x-auto{overflow-x:auto}.code{font-family:Consolas,monaco,monospace}.b{font-weight:700}.fw3{font-weight:300}.fw4{font-weight:400}.fw5{font-weight:500}.fw6{font-weight:600}.lh-copy{line-height:1.5}.link{text-decoration:none}.link,.link:active,.link:focus,.link:hover,.link:link,.link:visited{transition:color .15s ease-in}.link:focus{outline:1px dotted currentColor}.mw-100{max-width:100%}.mw4{max-width:8rem}.mw7{max-width:48rem}.bg-light-gray{background-color:#f7f7f7}.link-hover:hover{background-color:#1f679e}.white{color:#fff}.bg-white{background-color:#fff}.bg-blue{background-color:#408bc9}.pb2{padding-bottom:.5rem}.pb6{padding-bottom:8rem}.pt3{padding-top:1rem}.pt5{padding-top:4rem}.pv2{padding-top:.5rem;padding-bottom:.5rem}.ph3{padding-left:1rem;padding-right:1rem}.ph4{padding-left:2rem;padding-right:2rem}.ml0{margin-left:0}.mb1{margin-bottom:.25rem}.mb2{margin-bottom:.5rem}.mb3{margin-bottom:1rem}.mt5{margin-top:4rem}.ttu{text-transform:uppercase}.f4{font-size:1.25rem}.f5{font-size:1rem}.f6{font-size:.875rem}.f7{font-size:.75rem}.measure{max-width:30em}.center{margin-left:auto}.center{margin-right:auto}@media screen and (min-width:30em){.f2-ns{font-size:2.25rem}}@media screen and (min-width:30em) and (max-width:60em){.f5-m{font-size:1rem}}@media screen and (min-width:60em){.f4-l{font-size:1.25rem}}
|
||||
@@ -66,7 +66,7 @@ const indexTemplate = `
|
||||
</svg>
|
||||
<h1 class="f4 f2-ns mt5 fw5">Congrats! You created a tunnel!</h1>
|
||||
<p class="f6 f5-m f4-l measure lh-copy fw3">
|
||||
Argo Tunnel exposes locally running applications to the internet by
|
||||
Cloudflare Tunnel exposes locally running applications to the internet by
|
||||
running an encrypted, virtual tunnel from your laptop or server to
|
||||
Cloudflare's edge network.
|
||||
</p>
|
||||
@@ -74,7 +74,7 @@ const indexTemplate = `
|
||||
<a
|
||||
class="fw6 link white bg-blue ph4 pv2 br1 dib f5 link-hover"
|
||||
style="border-bottom: 1px solid #1f679e"
|
||||
href="https://developers.cloudflare.com/argo-tunnel/">
|
||||
href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps">
|
||||
Get started here
|
||||
</a>
|
||||
<section>
|
||||
|
||||
+2
-2
@@ -103,7 +103,7 @@ func NewWarpRoutingService() *WarpRoutingService {
|
||||
}
|
||||
|
||||
// Get a single origin service from the CLI/config.
|
||||
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (originService, error) {
|
||||
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginService, error) {
|
||||
if c.IsSet("hello-world") {
|
||||
return new(helloWorld), nil
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
|
||||
rules := make([]Rule, len(ingress))
|
||||
for i, r := range ingress {
|
||||
cfg := setConfig(defaults, r.OriginRequest)
|
||||
var service originService
|
||||
var service OriginService
|
||||
|
||||
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
|
||||
// No validation necessary for unix socket filepath services
|
||||
|
||||
@@ -48,7 +48,12 @@ type tcpOverWSConnection struct {
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
wc.streamHandler(websocket.NewConn(ctx, tunnelConn, log), wc.conn, log)
|
||||
wsCtx, cancel := context.WithCancel(ctx)
|
||||
wsConn := websocket.NewConn(wsCtx, tunnelConn, log)
|
||||
wc.streamHandler(wsConn, wc.conn, log)
|
||||
cancel()
|
||||
// Makes sure wsConn stops sending ping before terminating the stream
|
||||
wsConn.Close()
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Close() {
|
||||
@@ -63,7 +68,12 @@ type socksProxyOverWSConnection struct {
|
||||
}
|
||||
|
||||
func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
socks.StreamNetHandler(websocket.NewConn(ctx, tunnelConn, log), sp.accessPolicy, log)
|
||||
wsCtx, cancel := context.WithCancel(ctx)
|
||||
wsConn := websocket.NewConn(wsCtx, tunnelConn, log)
|
||||
socks.StreamNetHandler(wsConn, sp.accessPolicy, log)
|
||||
cancel()
|
||||
// Makes sure wsConn stops sending ping before terminating the stream
|
||||
wsConn.Close()
|
||||
}
|
||||
|
||||
func (sp *socksProxyOverWSConnection) Close() {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -20,6 +19,7 @@ import (
|
||||
"golang.org/x/net/proxy"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
@@ -190,69 +190,51 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamWSConnection(t *testing.T) {
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
func TestWsConnReturnsBeforeStreamReturns(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
eyeballConn, err := connection.NewHTTP2RespWriter(r, w, connection.TypeWebsocket)
|
||||
assert.NoError(t, err)
|
||||
|
||||
origin := echoWSOrigin(t, true)
|
||||
defer origin.Close()
|
||||
|
||||
var svc httpService
|
||||
err := svc.start(&sync.WaitGroup{}, testLogger, nil, nil, OriginRequestConfig{
|
||||
NoTLSVerify: true,
|
||||
cfdConn, originConn := net.Pipe()
|
||||
tcpOverWSConn := tcpOverWSConnection{
|
||||
conn: cfdConn,
|
||||
streamHandler: DefaultStreamHandler,
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
// Simulate losing connection to origin
|
||||
originConn.Close()
|
||||
}()
|
||||
ctx := context.WithValue(r.Context(), websocket.PingPeriodContextKey, time.Microsecond)
|
||||
tcpOverWSConn.Stream(ctx, eyeballConn, testLogger)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
client := server.Client()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, origin.URL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
|
||||
conn, resp, err := svc.newWebsocketProxyConnection(req)
|
||||
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
require.Equal(t, "Upgrade", resp.Header.Get("Connection"))
|
||||
require.Equal(t, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", resp.Header.Get("Sec-Websocket-Accept"))
|
||||
require.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
connClosed := make(chan struct{})
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
select {
|
||||
case <-connClosed:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
eyeballConn.Close()
|
||||
edgeConn.Close()
|
||||
conn.Close()
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodConnect, server.URL, edgeConn)
|
||||
assert.NoError(t, err)
|
||||
|
||||
return ctx.Err()
|
||||
})
|
||||
resp, err := client.Transport.RoundTrip(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, resp.StatusCode, http.StatusOK)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
echoWSEyeball(t, eyeballConn)
|
||||
fmt.Println("closing pipe")
|
||||
edgeConn.Close()
|
||||
return eyeballConn.Close()
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
for {
|
||||
if err := wsutil.WriteClientBinary(eyeballConn, testMessage); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
errGroup.Go(func() error {
|
||||
defer conn.Close()
|
||||
conn.Stream(ctx, edgeConn, testLogger)
|
||||
close(connClosed)
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, errGroup.Wait())
|
||||
assert.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
type wsEyeball struct {
|
||||
|
||||
+20
-117
@@ -1,20 +1,13 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
errUnsupportedConnectionType = errors.New("internal error: unsupported connection type")
|
||||
)
|
||||
|
||||
@@ -26,7 +19,7 @@ type HTTPOriginProxy interface {
|
||||
|
||||
// StreamBasedOriginProxy can be implemented by origin services that want to proxy ws/TCP.
|
||||
type StreamBasedOriginProxy interface {
|
||||
EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error)
|
||||
EstablishConnection(dest string) (OriginConnection, error)
|
||||
}
|
||||
|
||||
func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
@@ -36,7 +29,15 @@ func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
func (o *httpService) 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
|
||||
switch o.url.Scheme {
|
||||
case "ws":
|
||||
req.URL.Scheme = "http"
|
||||
case "wss":
|
||||
req.URL.Scheme = "https"
|
||||
default:
|
||||
req.URL.Scheme = o.url.Scheme
|
||||
}
|
||||
|
||||
if o.hostHeader != "" {
|
||||
// For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.
|
||||
req.Host = o.hostHeader
|
||||
@@ -44,138 +45,40 @@ func (o *httpService) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (o *httpService) EstablishConnection(req *http.Request) (OriginConnection, *http.Response, error) {
|
||||
req = req.Clone(req.Context())
|
||||
|
||||
req.URL.Host = o.url.Host
|
||||
req.URL.Scheme = o.url.Scheme
|
||||
// allow ws(s) scheme for websocket-only origins, normal http(s) requests will fail
|
||||
switch req.URL.Scheme {
|
||||
case "ws":
|
||||
req.URL.Scheme = "http"
|
||||
case "wss":
|
||||
req.URL.Scheme = "https"
|
||||
}
|
||||
|
||||
if o.hostHeader != "" {
|
||||
// For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.
|
||||
req.Host = o.hostHeader
|
||||
}
|
||||
|
||||
return o.newWebsocketProxyConnection(req)
|
||||
}
|
||||
|
||||
func (o *httpService) newWebsocketProxyConnection(req *http.Request) (OriginConnection, *http.Response, error) {
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Sec-WebSocket-Version", "13")
|
||||
|
||||
req.ContentLength = 0
|
||||
req.Body = nil
|
||||
|
||||
resp, err := o.transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
toClose := resp.Body
|
||||
defer func() {
|
||||
if toClose != nil {
|
||||
_ = toClose.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
return nil, nil, fmt.Errorf("unexpected origin response: %s", resp.Status)
|
||||
}
|
||||
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" {
|
||||
return nil, nil, fmt.Errorf("unexpected upgrade: %q", resp.Header.Get("Upgrade"))
|
||||
}
|
||||
|
||||
rwc, ok := resp.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
return nil, nil, errUnsupportedConnectionType
|
||||
}
|
||||
conn := wsProxyConnection{
|
||||
rwc: rwc,
|
||||
}
|
||||
// clear to prevent defer from closing
|
||||
toClose = nil
|
||||
|
||||
return &conn, resp, nil
|
||||
}
|
||||
|
||||
func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||
return o.resp, nil
|
||||
}
|
||||
|
||||
func (o *rawTCPService) EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
dest, err := getRequestHost(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
func (o *rawTCPService) EstablishConnection(dest string) (OriginConnection, error) {
|
||||
conn, err := net.Dial("tcp", dest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
originConn := &tcpConnection{
|
||||
conn: conn,
|
||||
}
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
ContentLength: -1,
|
||||
}
|
||||
return originConn, resp, nil
|
||||
return originConn, nil
|
||||
}
|
||||
|
||||
// getRequestHost returns the host of the http.Request.
|
||||
func getRequestHost(r *http.Request) (string, error) {
|
||||
if r.Host != "" {
|
||||
return r.Host, nil
|
||||
}
|
||||
if r.URL != nil {
|
||||
return r.URL.Host, nil
|
||||
}
|
||||
return "", errors.New("host not found")
|
||||
}
|
||||
|
||||
func (o *tcpOverWSService) EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
func (o *tcpOverWSService) EstablishConnection(dest string) (OriginConnection, error) {
|
||||
var err error
|
||||
dest := o.dest
|
||||
if o.isBastion {
|
||||
dest, err = carrier.ResolveBastionDest(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !o.isBastion {
|
||||
dest = o.dest
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", dest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
originConn := &tcpOverWSConnection{
|
||||
conn: conn,
|
||||
streamHandler: o.streamHandler,
|
||||
}
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
Header: websocket.NewResponseHeader(r),
|
||||
ContentLength: -1,
|
||||
}
|
||||
return originConn, resp, nil
|
||||
return originConn, nil
|
||||
|
||||
}
|
||||
|
||||
func (o *socksProxyOverWSService) EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
originConn := o.conn
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
Header: websocket.NewResponseHeader(r),
|
||||
ContentLength: -1,
|
||||
}
|
||||
return originConn, resp, nil
|
||||
func (o *socksProxyOverWSService) EstablishConnection(dest string) (OriginConnection, error) {
|
||||
return o.conn, nil
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -18,71 +17,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
// TestEstablishConnectionResponse ensures each implementation of StreamBasedOriginProxy returns
|
||||
// the expected response
|
||||
func assertEstablishConnectionResponse(t *testing.T,
|
||||
originProxy StreamBasedOriginProxy,
|
||||
req *http.Request,
|
||||
expectHeader http.Header,
|
||||
) {
|
||||
_, resp, err := originProxy.EstablishConnection(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, switchingProtocolText, resp.Status)
|
||||
assert.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
assert.Equal(t, expectHeader, resp.Header)
|
||||
}
|
||||
|
||||
func TestHTTPServiceEstablishConnection(t *testing.T) {
|
||||
origin := echoWSOrigin(t, false)
|
||||
defer origin.Close()
|
||||
originURL, err := url.Parse(origin.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpService := &httpService{
|
||||
url: originURL,
|
||||
hostHeader: origin.URL,
|
||||
transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, origin.URL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
req.Header.Set("Test-Cloudflared-Echo", t.Name())
|
||||
|
||||
expectHeader := http.Header{
|
||||
"Connection": {"Upgrade"},
|
||||
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
|
||||
"Upgrade": {"websocket"},
|
||||
"Test-Cloudflared-Echo": {t.Name()},
|
||||
}
|
||||
assertEstablishConnectionResponse(t, httpService, req, expectHeader)
|
||||
}
|
||||
|
||||
func TestHelloWorldEstablishConnection(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
shutdownC := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
helloWorldSerivce := &helloWorld{}
|
||||
helloWorldSerivce.start(&wg, testLogger, shutdownC, errC, OriginRequestConfig{})
|
||||
|
||||
// Scheme and Host of URL will be override by the Scheme and Host of the helloWorld service
|
||||
req, err := http.NewRequest(http.MethodGet, "https://place-holder/ws", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
|
||||
expectHeader := http.Header{
|
||||
"Connection": {"Upgrade"},
|
||||
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
|
||||
"Upgrade": {"websocket"},
|
||||
}
|
||||
assertEstablishConnectionResponse(t, helloWorldSerivce, req, expectHeader)
|
||||
|
||||
close(shutdownC)
|
||||
}
|
||||
|
||||
func TestRawTCPServiceEstablishConnection(t *testing.T) {
|
||||
originListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
@@ -95,8 +29,6 @@ func TestRawTCPServiceEstablishConnection(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originListener.Addr()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertEstablishConnectionResponse(t, rawTCPService, req, nil)
|
||||
|
||||
originListener.Close()
|
||||
<-listenerClosed
|
||||
|
||||
@@ -104,9 +36,8 @@ func TestRawTCPServiceEstablishConnection(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Origin not listening for new connection, should return an error
|
||||
_, resp, err := rawTCPService.EstablishConnection(req)
|
||||
_, err = rawTCPService.EstablishConnection(req.URL.String())
|
||||
require.Error(t, err)
|
||||
require.Nil(t, resp)
|
||||
}
|
||||
|
||||
func TestTCPOverWSServiceEstablishConnection(t *testing.T) {
|
||||
@@ -128,12 +59,6 @@ func TestTCPOverWSServiceEstablishConnection(t *testing.T) {
|
||||
bastionReq := baseReq.Clone(context.Background())
|
||||
carrier.SetBastionDest(bastionReq.Header, originListener.Addr().String())
|
||||
|
||||
expectHeader := http.Header{
|
||||
"Connection": {"Upgrade"},
|
||||
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
|
||||
"Upgrade": {"websocket"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
testCase string
|
||||
service *tcpOverWSService
|
||||
@@ -161,11 +86,9 @@ func TestTCPOverWSServiceEstablishConnection(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.testCase, func(t *testing.T) {
|
||||
if test.expectErr {
|
||||
_, resp, err := test.service.EstablishConnection(test.req)
|
||||
bastionHost, _ := carrier.ResolveBastionDest(test.req)
|
||||
_, err := test.service.EstablishConnection(bastionHost)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
} else {
|
||||
assertEstablishConnectionResponse(t, test.service, test.req, expectHeader)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -175,9 +98,9 @@ func TestTCPOverWSServiceEstablishConnection(t *testing.T) {
|
||||
|
||||
for _, service := range []*tcpOverWSService{newTCPOverWSService(originURL), newBastionService()} {
|
||||
// Origin not listening for new connection, should return an error
|
||||
_, resp, err := service.EstablishConnection(bastionReq)
|
||||
bastionHost, _ := carrier.ResolveBastionDest(bastionReq)
|
||||
_, err := service.EstablishConnection(bastionHost)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,10 +141,6 @@ func TestHTTPServiceHostHeaderOverride(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
req = req.Clone(context.Background())
|
||||
_, resp, err = httpService.EstablishConnection(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
}
|
||||
|
||||
func tcpListenRoutine(listener net.Listener, closeChan chan struct{}) {
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
)
|
||||
|
||||
// originService is something a tunnel can proxy traffic to.
|
||||
type originService interface {
|
||||
// OriginService is something a tunnel can proxy traffic to.
|
||||
type OriginService interface {
|
||||
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
|
||||
@@ -238,7 +238,7 @@ func (nrc *NopReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHTTPTransport(service originService, cfg OriginRequestConfig, log *zerolog.Logger) (*http.Transport, error) {
|
||||
func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerolog.Logger) (*http.Transport, error) {
|
||||
originCertPool, err := tlsconfig.LoadOriginCA(cfg.CAPool, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ type Rule struct {
|
||||
// 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
|
||||
Service OriginService
|
||||
|
||||
// Configure the request cloudflared sends to this specific origin.
|
||||
Config OriginRequestConfig
|
||||
|
||||
@@ -14,7 +14,7 @@ func Test_rule_matches(t *testing.T) {
|
||||
type fields struct {
|
||||
Hostname string
|
||||
Path *regexp.Regexp
|
||||
Service originService
|
||||
Service OriginService
|
||||
}
|
||||
type args struct {
|
||||
requestURL *url.URL
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
basename: "cloudflared"
|
||||
comment: "Cloudflare Argo Tunnel"
|
||||
comment: "Cloudflare Tunnel"
|
||||
copyright: "Cloudflare, Inc"
|
||||
arch: "x86"
|
||||
abi: "64"
|
||||
|
||||
+6
-2
@@ -22,7 +22,7 @@ const (
|
||||
startupTime = time.Millisecond * 500
|
||||
)
|
||||
|
||||
func newMetricsHandler(readyServer *ReadyServer) *mux.Router {
|
||||
func newMetricsHandler(readyServer *ReadyServer, quickTunnelHostname string) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
router.PathPrefix("/debug/").Handler(http.DefaultServeMux)
|
||||
|
||||
@@ -33,6 +33,9 @@ func newMetricsHandler(readyServer *ReadyServer) *mux.Router {
|
||||
if readyServer != nil {
|
||||
router.Handle("/ready", readyServer)
|
||||
}
|
||||
router.HandleFunc("/quicktunnel", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, `{"hostname":"%s"}`, quickTunnelHostname)
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -41,6 +44,7 @@ func ServeMetrics(
|
||||
l net.Listener,
|
||||
shutdownC <-chan struct{},
|
||||
readyServer *ReadyServer,
|
||||
quickTunnelHostname string,
|
||||
log *zerolog.Logger,
|
||||
) (err error) {
|
||||
var wg sync.WaitGroup
|
||||
@@ -48,7 +52,7 @@ func ServeMetrics(
|
||||
trace.AuthRequest = func(*http.Request) (bool, bool) { return true, true }
|
||||
// TODO: parameterize ReadTimeout and WriteTimeout. The maximum time we can
|
||||
// profile CPU usage depends on WriteTimeout
|
||||
h := newMetricsHandler(readyServer)
|
||||
h := newMetricsHandler(readyServer, quickTunnelHostname)
|
||||
server := &http.Server{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
|
||||
@@ -36,8 +36,6 @@ func (rs *ReadyServer) OnTunnelEvent(c conn.Event) {
|
||||
rs.Lock()
|
||||
rs.isConnected[int(c.Index)] = false
|
||||
rs.Unlock()
|
||||
case conn.SetURL:
|
||||
break
|
||||
default:
|
||||
rs.log.Error().Msgf("Unknown connection event case %v", c)
|
||||
}
|
||||
|
||||
+154
-94
@@ -7,23 +7,27 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/carrier"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
TagHeaderNamePrefix = "Cf-Warp-Tag-"
|
||||
LogFieldCFRay = "cfRay"
|
||||
LogFieldRule = "ingressRule"
|
||||
// TagHeaderNamePrefix indicates a Cloudflared Warp Tag prefix that gets appended for warp traffic stream headers.
|
||||
TagHeaderNamePrefix = "Cf-Warp-Tag-"
|
||||
LogFieldCFRay = "cfRay"
|
||||
LogFieldRule = "ingressRule"
|
||||
LogFieldOriginService = "originService"
|
||||
)
|
||||
|
||||
type proxy struct {
|
||||
// Proxy represents a means to Proxy between cloudflared and the origin services.
|
||||
type Proxy struct {
|
||||
ingressRules ingress.Ingress
|
||||
warpRouting *ingress.WarpRoutingService
|
||||
tags []tunnelpogs.Tag
|
||||
@@ -31,13 +35,14 @@ type proxy struct {
|
||||
bufferPool *bufferPool
|
||||
}
|
||||
|
||||
// NewOriginProxy returns a new instance of the Proxy struct.
|
||||
func NewOriginProxy(
|
||||
ingressRules ingress.Ingress,
|
||||
warpRouting *ingress.WarpRoutingService,
|
||||
tags []tunnelpogs.Tag,
|
||||
log *zerolog.Logger) connection.OriginProxy {
|
||||
|
||||
return &proxy{
|
||||
log *zerolog.Logger,
|
||||
) *Proxy {
|
||||
return &Proxy{
|
||||
ingressRules: ingressRules,
|
||||
warpRouting: warpRouting,
|
||||
tags: tags,
|
||||
@@ -46,35 +51,19 @@ func NewOriginProxy(
|
||||
}
|
||||
}
|
||||
|
||||
// Caller is responsible for writing any error to ResponseWriter
|
||||
func (p *proxy) Proxy(w connection.ResponseWriter, req *http.Request, sourceConnectionType connection.Type) error {
|
||||
// ProxyHTTP further depends on ingress rules to establish a connection with the origin service. This may be
|
||||
// a simple roundtrip or a tcp/websocket dial depending on ingres rule setup.
|
||||
func (p *Proxy) ProxyHTTP(
|
||||
w connection.ResponseWriter,
|
||||
req *http.Request,
|
||||
isWebsocket bool,
|
||||
) error {
|
||||
incrementRequests()
|
||||
defer decrementConcurrentRequests()
|
||||
|
||||
cfRay := findCfRayHeader(req)
|
||||
lbProbe := isLBProbeRequest(req)
|
||||
|
||||
serveCtx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
||||
cfRay := connection.FindCfRayHeader(req)
|
||||
lbProbe := connection.IsLBProbeRequest(req)
|
||||
p.appendTagHeaders(req)
|
||||
if sourceConnectionType == connection.TypeTCP {
|
||||
if p.warpRouting == nil {
|
||||
err := errors.New(`cloudflared received a request from WARP client, but your configuration has disabled ingress from WARP clients. To enable this, set "warp-routing:\n\t enabled: true" in your config.yaml`)
|
||||
p.log.Error().Msg(err.Error())
|
||||
return err
|
||||
}
|
||||
logFields := logFields{
|
||||
cfRay: cfRay,
|
||||
lbProbe: lbProbe,
|
||||
rule: ingress.ServiceWarpRouting,
|
||||
}
|
||||
if err := p.proxyStreamRequest(serveCtx, w, req, p.warpRouting.Proxy, logFields); err != nil {
|
||||
p.logRequestError(err, cfRay, ingress.ServiceWarpRouting)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
rule, ruleNum := p.ingressRules.FindMatchingRule(req.Host, req.URL.Path)
|
||||
logFields := logFields{
|
||||
@@ -84,56 +73,113 @@ func (p *proxy) Proxy(w connection.ResponseWriter, req *http.Request, sourceConn
|
||||
}
|
||||
p.logRequest(req, logFields)
|
||||
|
||||
if sourceConnectionType == connection.TypeHTTP {
|
||||
if err := p.proxyHTTPRequest(w, req, rule, logFields); err != nil {
|
||||
p.logRequestError(err, cfRay, ruleField(p.ingressRules, ruleNum))
|
||||
switch originProxy := rule.Service.(type) {
|
||||
case ingress.HTTPOriginProxy:
|
||||
if err := p.proxyHTTPRequest(
|
||||
w,
|
||||
req,
|
||||
originProxy,
|
||||
isWebsocket,
|
||||
rule.Config.DisableChunkedEncoding,
|
||||
logFields,
|
||||
); err != nil {
|
||||
rule, srv := ruleField(p.ingressRules, ruleNum)
|
||||
p.logRequestError(err, cfRay, rule, srv)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
connectionProxy, ok := rule.Service.(ingress.StreamBasedOriginProxy)
|
||||
if !ok {
|
||||
p.log.Error().Msgf("%s is not a connection-oriented service", rule.Service)
|
||||
return fmt.Errorf("Not a connection-oriented service")
|
||||
}
|
||||
case ingress.StreamBasedOriginProxy:
|
||||
dest, err := getDestFromRule(rule, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.proxyStreamRequest(serveCtx, w, req, connectionProxy, logFields); err != nil {
|
||||
p.logRequestError(err, cfRay, ruleField(p.ingressRules, ruleNum))
|
||||
rws := connection.NewHTTPResponseReadWriterAcker(w, req)
|
||||
if err := p.proxyStream(req.Context(), rws, dest, originProxy, logFields); err != nil {
|
||||
rule, srv := ruleField(p.ingressRules, ruleNum)
|
||||
p.logRequestError(err, cfRay, rule, srv)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized service: %s, %t", rule.Service, originProxy)
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyTCP proxies to a TCP connection between the origin service and cloudflared.
|
||||
func (p *Proxy) ProxyTCP(
|
||||
ctx context.Context,
|
||||
rwa connection.ReadWriteAcker,
|
||||
req *connection.TCPRequest,
|
||||
) error {
|
||||
incrementRequests()
|
||||
defer decrementConcurrentRequests()
|
||||
|
||||
if p.warpRouting == nil {
|
||||
err := errors.New(`cloudflared received a request from WARP client, but your configuration has disabled ingress from WARP clients. To enable this, set "warp-routing:\n\t enabled: true" in your config.yaml`)
|
||||
p.log.Error().Msg(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
serveCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
logFields := logFields{
|
||||
cfRay: req.CFRay,
|
||||
lbProbe: req.LBProbe,
|
||||
rule: ingress.ServiceWarpRouting,
|
||||
}
|
||||
|
||||
if err := p.proxyStream(serveCtx, rwa, req.Dest, p.warpRouting.Proxy, logFields); err != nil {
|
||||
p.logRequestError(err, req.CFRay, "", ingress.ServiceWarpRouting)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ruleField(ing ingress.Ingress, ruleNum int) string {
|
||||
func ruleField(ing ingress.Ingress, ruleNum int) (ruleID string, srv string) {
|
||||
srv = ing.Rules[ruleNum].Service.String()
|
||||
if ing.IsSingleRule() {
|
||||
return ""
|
||||
return "", srv
|
||||
}
|
||||
return fmt.Sprintf("%d", ruleNum)
|
||||
return fmt.Sprintf("%d", ruleNum), srv
|
||||
}
|
||||
|
||||
func (p *proxy) proxyHTTPRequest(w connection.ResponseWriter, req *http.Request, rule *ingress.Rule, fields logFields) error {
|
||||
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
|
||||
if rule.Config.DisableChunkedEncoding {
|
||||
req.TransferEncoding = []string{"gzip", "deflate"}
|
||||
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
|
||||
if err == nil {
|
||||
req.ContentLength = int64(cLength)
|
||||
// ProxyHTTPRequest proxies requests of underlying type http and websocket to the origin service.
|
||||
func (p *Proxy) proxyHTTPRequest(
|
||||
w connection.ResponseWriter,
|
||||
req *http.Request,
|
||||
httpService ingress.HTTPOriginProxy,
|
||||
isWebsocket bool,
|
||||
disableChunkedEncoding bool,
|
||||
fields logFields,
|
||||
) error {
|
||||
roundTripReq := req
|
||||
if isWebsocket {
|
||||
roundTripReq = req.Clone(req.Context())
|
||||
roundTripReq.Header.Set("Connection", "Upgrade")
|
||||
roundTripReq.Header.Set("Upgrade", "websocket")
|
||||
roundTripReq.Header.Set("Sec-Websocket-Version", "13")
|
||||
roundTripReq.ContentLength = 0
|
||||
roundTripReq.Body = nil
|
||||
} else {
|
||||
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
|
||||
if disableChunkedEncoding {
|
||||
roundTripReq.TransferEncoding = []string{"gzip", "deflate"}
|
||||
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
|
||||
if err == nil {
|
||||
roundTripReq.ContentLength = int64(cLength)
|
||||
}
|
||||
}
|
||||
// Request origin to keep connection alive to improve performance
|
||||
roundTripReq.Header.Set("Connection", "keep-alive")
|
||||
}
|
||||
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
httpService, ok := rule.Service.(ingress.HTTPOriginProxy)
|
||||
if !ok {
|
||||
p.log.Error().Msgf("%s is not a http service", rule.Service)
|
||||
return fmt.Errorf("Not a http service")
|
||||
}
|
||||
|
||||
resp, err := httpService.RoundTrip(req)
|
||||
resp, err := httpService.RoundTrip(roundTripReq)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error proxying request to origin")
|
||||
return errors.Wrap(err, "Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -141,6 +187,23 @@ func (p *proxy) proxyHTTPRequest(w connection.ResponseWriter, req *http.Request,
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusSwitchingProtocols {
|
||||
rwc, ok := resp.Body.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
return errors.New("internal error: unsupported connection type")
|
||||
}
|
||||
defer rwc.Close()
|
||||
|
||||
eyeballStream := &bidirectionalStream{
|
||||
writer: w,
|
||||
reader: req.Body,
|
||||
}
|
||||
|
||||
websocket.Stream(eyeballStream, rwc, p.log)
|
||||
return nil
|
||||
}
|
||||
|
||||
if connection.IsServerSentEvent(resp.Header) {
|
||||
p.log.Debug().Msg("Detected Server-Side Events from Origin")
|
||||
p.writeEventStream(w, resp.Body)
|
||||
@@ -151,32 +214,30 @@ func (p *proxy) proxyHTTPRequest(w connection.ResponseWriter, req *http.Request,
|
||||
defer p.bufferPool.Put(buf)
|
||||
_, _ = io.CopyBuffer(w, resp.Body, buf)
|
||||
}
|
||||
|
||||
p.logOriginResponse(resp, fields)
|
||||
return nil
|
||||
}
|
||||
|
||||
// proxyStreamRequest first establish a connection with origin, then it writes the status code and headers, and finally it streams data between
|
||||
// eyeball and origin.
|
||||
func (p *proxy) proxyStreamRequest(
|
||||
serveCtx context.Context,
|
||||
w connection.ResponseWriter,
|
||||
req *http.Request,
|
||||
// proxyStream proxies type TCP and other underlying types if the connection is defined as a stream oriented
|
||||
// ingress rule.
|
||||
func (p *Proxy) proxyStream(
|
||||
ctx context.Context,
|
||||
rwa connection.ReadWriteAcker,
|
||||
dest string,
|
||||
connectionProxy ingress.StreamBasedOriginProxy,
|
||||
fields logFields,
|
||||
) error {
|
||||
originConn, resp, err := connectionProxy.EstablishConnection(req)
|
||||
originConn, err := connectionProxy.EstablishConnection(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err = w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
|
||||
if err := rwa.AckConnection(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamCtx, cancel := context.WithCancel(serveCtx)
|
||||
streamCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
@@ -185,12 +246,7 @@ func (p *proxy) proxyStreamRequest(
|
||||
originConn.Close()
|
||||
}()
|
||||
|
||||
eyeballStream := &bidirectionalStream{
|
||||
writer: w,
|
||||
reader: req.Body,
|
||||
}
|
||||
originConn.Stream(serveCtx, eyeballStream, p.log)
|
||||
p.logOriginResponse(resp, fields)
|
||||
originConn.Stream(ctx, rwa, p.log)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -207,7 +263,7 @@ func (wr *bidirectionalStream) Write(p []byte) (n int, err error) {
|
||||
return wr.writer.Write(p)
|
||||
}
|
||||
|
||||
func (p *proxy) writeEventStream(w connection.ResponseWriter, respBody io.ReadCloser) {
|
||||
func (p *Proxy) writeEventStream(w connection.ResponseWriter, respBody io.ReadCloser) {
|
||||
reader := bufio.NewReader(respBody)
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
@@ -218,7 +274,7 @@ func (p *proxy) writeEventStream(w connection.ResponseWriter, respBody io.ReadCl
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxy) appendTagHeaders(r *http.Request) {
|
||||
func (p *Proxy) appendTagHeaders(r *http.Request) {
|
||||
for _, tag := range p.tags {
|
||||
r.Header.Add(TagHeaderNamePrefix+tag.Name, tag.Value)
|
||||
}
|
||||
@@ -230,7 +286,7 @@ type logFields struct {
|
||||
rule interface{}
|
||||
}
|
||||
|
||||
func (p *proxy) logRequest(r *http.Request, fields logFields) {
|
||||
func (p *Proxy) logRequest(r *http.Request, fields logFields) {
|
||||
if fields.cfRay != "" {
|
||||
p.log.Debug().Msgf("CF-RAY: %s %s %s %s", fields.cfRay, r.Method, r.URL, r.Proto)
|
||||
} else if fields.lbProbe {
|
||||
@@ -253,7 +309,7 @@ func (p *proxy) logRequest(r *http.Request, fields logFields) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxy) logOriginResponse(resp *http.Response, fields logFields) {
|
||||
func (p *Proxy) logOriginResponse(resp *http.Response, fields logFields) {
|
||||
responseByCode.WithLabelValues(strconv.Itoa(resp.StatusCode)).Inc()
|
||||
if fields.cfRay != "" {
|
||||
p.log.Debug().Msgf("CF-RAY: %s Status: %s served by ingress %d", fields.cfRay, resp.Status, fields.rule)
|
||||
@@ -271,7 +327,7 @@ func (p *proxy) logOriginResponse(resp *http.Response, fields logFields) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxy) logRequestError(err error, cfRay string, rule string) {
|
||||
func (p *Proxy) logRequestError(err error, cfRay string, rule, service string) {
|
||||
requestErrors.Inc()
|
||||
log := p.log.Error().Err(err)
|
||||
if cfRay != "" {
|
||||
@@ -280,13 +336,17 @@ func (p *proxy) logRequestError(err error, cfRay string, rule string) {
|
||||
if rule != "" {
|
||||
log = log.Str(LogFieldRule, rule)
|
||||
}
|
||||
if service != "" {
|
||||
log = log.Str(LogFieldOriginService, service)
|
||||
}
|
||||
log.Msg("")
|
||||
}
|
||||
|
||||
func findCfRayHeader(req *http.Request) string {
|
||||
return req.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
func isLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
func getDestFromRule(rule *ingress.Rule, req *http.Request) (string, error) {
|
||||
switch rule.Service.String() {
|
||||
case ingress.ServiceBastion:
|
||||
return carrier.ResolveBastionDest(req)
|
||||
default:
|
||||
return rule.Service.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// +build !windows
|
||||
|
||||
package origin
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
)
|
||||
|
||||
func TestUnixSocketOrigin(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "unix.sock")
|
||||
require.NoError(t, err)
|
||||
os.Remove(file.Name()) // remove the file since binding the socket expects to create it
|
||||
|
||||
l, err := net.Listen("unix", file.Name())
|
||||
require.NoError(t, err)
|
||||
defer l.Close()
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
api := &httptest.Server{
|
||||
Listener: l,
|
||||
Config: &http.Server{Handler: mockAPI{}},
|
||||
}
|
||||
api.Start()
|
||||
defer api.Close()
|
||||
|
||||
unvalidatedIngress := []config.UnvalidatedIngressRule{
|
||||
{
|
||||
Hostname: "unix.example.com",
|
||||
Service: "unix:" + file.Name(),
|
||||
},
|
||||
{
|
||||
Hostname: "*",
|
||||
Service: "http_status:404",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []MultipleIngressTest{
|
||||
{
|
||||
url: "http://unix.example.com",
|
||||
expectedStatus: http.StatusCreated,
|
||||
expectedBody: []byte("Created"),
|
||||
},
|
||||
}
|
||||
|
||||
runIngressTestScenarios(t, unvalidatedIngress, tests)
|
||||
}
|
||||
+62
-32
@@ -30,7 +30,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
testTags = []tunnelpogs.Tag(nil)
|
||||
testTags = []tunnelpogs.Tag{tunnelpogs.Tag{Name: "Name", Value: "value"}}
|
||||
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
|
||||
)
|
||||
|
||||
@@ -44,6 +44,10 @@ func newMockHTTPRespWriter() *mockHTTPRespWriter {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mockHTTPRespWriter) WriteResponse() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *mockHTTPRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
w.WriteHeader(status)
|
||||
for header, val := range header {
|
||||
@@ -144,8 +148,11 @@ func testProxyHTTP(proxy connection.OriginProxy) func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeHTTP)
|
||||
err = proxy.ProxyHTTP(responseWriter, req, false)
|
||||
require.NoError(t, err)
|
||||
for _, tag := range testTags {
|
||||
assert.Equal(t, tag.Value, req.Header.Get(TagHeaderNamePrefix+tag.Name))
|
||||
}
|
||||
|
||||
assert.Equal(t, http.StatusOK, responseWriter.Code)
|
||||
}
|
||||
@@ -168,7 +175,7 @@ func testProxyWebsocket(proxy connection.OriginProxy) func(t *testing.T) {
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeWebsocket)
|
||||
err = proxy.ProxyHTTP(responseWriter, req, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, http.StatusSwitchingProtocols, responseWriter.Code)
|
||||
@@ -229,7 +236,7 @@ func testProxySSE(proxy connection.OriginProxy) func(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeHTTP)
|
||||
err = proxy.ProxyHTTP(responseWriter, req, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, http.StatusOK, responseWriter.Code)
|
||||
@@ -273,26 +280,7 @@ func TestProxyMultipleOrigins(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ingress, err := ingress.ParseIngress(&config.Configuration{
|
||||
TunnelID: t.Name(),
|
||||
Ingress: unvalidatedIngress,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
log := zerolog.Nop()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
var wg sync.WaitGroup
|
||||
require.NoError(t, ingress.StartOrigins(&wg, &log, ctx.Done(), errC))
|
||||
|
||||
proxy := NewOriginProxy(ingress, unusedWarpRoutingService, testTags, &log)
|
||||
|
||||
tests := []struct {
|
||||
url string
|
||||
expectedStatus int
|
||||
expectedBody []byte
|
||||
}{
|
||||
tests := []MultipleIngressTest{
|
||||
{
|
||||
url: "http://api.example.com",
|
||||
expectedStatus: http.StatusCreated,
|
||||
@@ -317,12 +305,37 @@ func TestProxyMultipleOrigins(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
runIngressTestScenarios(t, unvalidatedIngress, tests)
|
||||
}
|
||||
|
||||
type MultipleIngressTest struct {
|
||||
url string
|
||||
expectedStatus int
|
||||
expectedBody []byte
|
||||
}
|
||||
|
||||
func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.UnvalidatedIngressRule, tests []MultipleIngressTest) {
|
||||
ingress, err := ingress.ParseIngress(&config.Configuration{
|
||||
TunnelID: t.Name(),
|
||||
Ingress: unvalidatedIngress,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
log := zerolog.Nop()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
var wg sync.WaitGroup
|
||||
require.NoError(t, ingress.StartOrigins(&wg, &log, ctx.Done(), errC))
|
||||
|
||||
proxy := NewOriginProxy(ingress, unusedWarpRoutingService, testTags, &log)
|
||||
|
||||
for _, test := range tests {
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
req, err := http.NewRequest(http.MethodGet, test.url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeHTTP)
|
||||
err = proxy.ProxyHTTP(responseWriter, req, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expectedStatus, responseWriter.Code)
|
||||
@@ -350,7 +363,7 @@ func (errorOriginTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func TestProxyError(t *testing.T) {
|
||||
ingress := ingress.Ingress{
|
||||
ing := ingress.Ingress{
|
||||
Rules: []ingress.Rule{
|
||||
{
|
||||
Hostname: "*",
|
||||
@@ -364,13 +377,13 @@ func TestProxyError(t *testing.T) {
|
||||
|
||||
log := zerolog.Nop()
|
||||
|
||||
proxy := NewOriginProxy(ingress, unusedWarpRoutingService, testTags, &log)
|
||||
proxy := NewOriginProxy(ing, unusedWarpRoutingService, testTags, &log)
|
||||
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Error(t, proxy.Proxy(responseWriter, req, connection.TypeHTTP))
|
||||
assert.Error(t, proxy.ProxyHTTP(responseWriter, req, false))
|
||||
}
|
||||
|
||||
type replayer struct {
|
||||
@@ -563,8 +576,14 @@ func TestConnections(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte{},
|
||||
err: true,
|
||||
message: []byte("Forbidden\n"),
|
||||
err: false,
|
||||
headers: map[string][]string{
|
||||
"Content-Length": {"10"},
|
||||
"Content-Type": {"text/plain; charset=utf-8"},
|
||||
"Sec-Websocket-Version": {"13"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -603,6 +622,7 @@ func TestConnections(t *testing.T) {
|
||||
ingressRule.StartOrigins(&wg, logger, ctx.Done(), errC)
|
||||
proxy := NewOriginProxy(ingressRule, test.args.warpRoutingService, testTags, logger)
|
||||
|
||||
dest := ln.Addr().String()
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
test.args.ingressServiceScheme+ln.Addr().String(),
|
||||
@@ -620,8 +640,12 @@ func TestConnections(t *testing.T) {
|
||||
replayer.Write(resp)
|
||||
}()
|
||||
}
|
||||
|
||||
err = proxy.Proxy(respWriter, req, test.args.connectionType)
|
||||
if test.args.connectionType == connection.TypeTCP {
|
||||
rws := connection.NewHTTPResponseReadWriterAcker(respWriter, req)
|
||||
err = proxy.ProxyTCP(ctx, rws, &connection.TCPRequest{Dest: dest})
|
||||
} else {
|
||||
err = proxy.ProxyHTTP(respWriter, req, test.args.connectionType == connection.TypeWebsocket)
|
||||
}
|
||||
|
||||
cancel()
|
||||
assert.Equal(t, test.want.err, err != nil)
|
||||
@@ -759,6 +783,8 @@ func (w *wsRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
|
||||
// respHeaders is a test function to read respHeaders
|
||||
func (w *wsRespWriter) headers() http.Header {
|
||||
// Removing indeterminstic header because it cannot be asserted.
|
||||
w.responseHeaders.Del("Date")
|
||||
return w.responseHeaders
|
||||
}
|
||||
|
||||
@@ -774,6 +800,10 @@ func newTCPRespWriter(w io.Writer) *mockTCPRespWriter {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockTCPRespWriter) Read(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (m *mockTCPRespWriter) Write(p []byte) (n int, err error) {
|
||||
return m.w.Write(p)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
@@ -60,7 +60,7 @@ var errEarlyShutdown = errors.New("shutdown started")
|
||||
|
||||
type tunnelError struct {
|
||||
index int
|
||||
addr *net.TCPAddr
|
||||
addr *allregions.EdgeAddr
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func NewSupervisor(config *TunnelConfig, reconnectCh chan ReconnectSignal, grace
|
||||
if len(config.EdgeAddrs) > 0 {
|
||||
edgeIPs, err = edgediscovery.StaticEdge(config.Log, config.EdgeAddrs)
|
||||
} else {
|
||||
edgeIPs, err = edgediscovery.ResolveEdge(config.Log)
|
||||
edgeIPs, err = edgediscovery.ResolveEdge(config.Log, config.Region)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -226,7 +226,7 @@ func (s *Supervisor) startFirstTunnel(
|
||||
connectedSignal *signal.Signal,
|
||||
) {
|
||||
var (
|
||||
addr *net.TCPAddr
|
||||
addr *allregions.EdgeAddr
|
||||
err error
|
||||
)
|
||||
const firstConnIndex = 0
|
||||
@@ -294,7 +294,7 @@ func (s *Supervisor) startTunnel(
|
||||
connectedSignal *signal.Signal,
|
||||
) {
|
||||
var (
|
||||
addr *net.TCPAddr
|
||||
addr *allregions.EdgeAddr
|
||||
err error
|
||||
)
|
||||
defer func() {
|
||||
@@ -347,7 +347,7 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, s.config.EdgeTLSConfigs[connection.H2mux], arbitraryEdgeIP)
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, s.config.EdgeTLSConfigs[connection.H2mux], arbitraryEdgeIP.TCP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+173
-47
@@ -11,13 +11,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc"
|
||||
@@ -26,9 +29,10 @@ import (
|
||||
|
||||
const (
|
||||
dialTimeout = 15 * time.Second
|
||||
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
|
||||
FeatureSerializedHeaders = "serialized_headers"
|
||||
FeatureQuickReconnects = "quick_reconnects"
|
||||
quicHandshakeIdleTimeout = 5 * time.Second
|
||||
quicMaxIdleTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type rpcName string
|
||||
@@ -44,10 +48,10 @@ type TunnelConfig struct {
|
||||
ClientID string
|
||||
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
|
||||
EdgeAddrs []string
|
||||
Region string
|
||||
HAConnections int
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
IsFreeTunnel bool
|
||||
LBPool string
|
||||
Tags []tunnelpogs.Tag
|
||||
Log *zerolog.Logger
|
||||
@@ -126,7 +130,7 @@ func ServeTunnelLoop(
|
||||
ctx context.Context,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
addr *net.TCPAddr,
|
||||
addr *allregions.EdgeAddr,
|
||||
connIndex uint8,
|
||||
connectedSignal *signal.Signal,
|
||||
cloudflaredUUID uuid.UUID,
|
||||
@@ -247,7 +251,7 @@ func ServeTunnel(
|
||||
connLog *zerolog.Logger,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
addr *net.TCPAddr,
|
||||
addr *allregions.EdgeAddr,
|
||||
connIndex uint8,
|
||||
fuse *h2mux.BooleanFuse,
|
||||
backoff *protocolFallback,
|
||||
@@ -270,44 +274,20 @@ func ServeTunnel(
|
||||
}()
|
||||
|
||||
defer config.Observer.SendDisconnect(connIndex)
|
||||
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, config.EdgeTLSConfigs[protocol], addr)
|
||||
if err != nil {
|
||||
connLog.Err(err).Msg("Unable to establish connection with Cloudflare edge")
|
||||
return err, true
|
||||
}
|
||||
connectedFuse := &connectedFuse{
|
||||
fuse: fuse,
|
||||
backoff: backoff,
|
||||
}
|
||||
|
||||
if protocol == connection.HTTP2 {
|
||||
connOptions := config.ConnectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries()))
|
||||
err = ServeHTTP2(
|
||||
ctx,
|
||||
connLog,
|
||||
config,
|
||||
edgeConn,
|
||||
connOptions,
|
||||
connIndex,
|
||||
connectedFuse,
|
||||
reconnectCh,
|
||||
gracefulShutdownC,
|
||||
)
|
||||
} else {
|
||||
err = ServeH2mux(
|
||||
ctx,
|
||||
connLog,
|
||||
credentialManager,
|
||||
config,
|
||||
edgeConn,
|
||||
connIndex,
|
||||
connectedFuse,
|
||||
cloudflaredUUID,
|
||||
reconnectCh,
|
||||
gracefulShutdownC,
|
||||
)
|
||||
}
|
||||
err, recoverable = serveTunnel(
|
||||
ctx,
|
||||
connLog,
|
||||
credentialManager,
|
||||
config,
|
||||
addr,
|
||||
connIndex,
|
||||
fuse,
|
||||
backoff,
|
||||
cloudflaredUUID,
|
||||
reconnectCh,
|
||||
protocol,
|
||||
gracefulShutdownC,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
@@ -342,6 +322,94 @@ func ServeTunnel(
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func serveTunnel(
|
||||
ctx context.Context,
|
||||
connLog *zerolog.Logger,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
addr *allregions.EdgeAddr,
|
||||
connIndex uint8,
|
||||
fuse *h2mux.BooleanFuse,
|
||||
backoff *protocolFallback,
|
||||
cloudflaredUUID uuid.UUID,
|
||||
reconnectCh chan ReconnectSignal,
|
||||
protocol connection.Protocol,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) (err error, recoverable bool) {
|
||||
|
||||
connectedFuse := &connectedFuse{
|
||||
fuse: fuse,
|
||||
backoff: backoff,
|
||||
}
|
||||
controlStream := connection.NewControlStream(
|
||||
config.Observer,
|
||||
connectedFuse,
|
||||
config.NamedTunnel,
|
||||
connIndex,
|
||||
nil,
|
||||
gracefulShutdownC,
|
||||
config.ConnectionConfig.GracePeriod,
|
||||
)
|
||||
|
||||
switch protocol {
|
||||
case connection.QUIC, connection.QUICWarp:
|
||||
connOptions := config.ConnectionOptions(addr.UDP.String(), uint8(backoff.Retries()))
|
||||
return ServeQUIC(ctx,
|
||||
addr.UDP,
|
||||
config,
|
||||
connOptions,
|
||||
controlStream,
|
||||
connIndex,
|
||||
reconnectCh,
|
||||
gracefulShutdownC)
|
||||
|
||||
case connection.HTTP2, connection.HTTP2Warp:
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, config.EdgeTLSConfigs[protocol], addr.TCP)
|
||||
if err != nil {
|
||||
connLog.Err(err).Msg("Unable to establish connection with Cloudflare edge")
|
||||
return err, true
|
||||
}
|
||||
|
||||
connOptions := config.ConnectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries()))
|
||||
if err := ServeHTTP2(
|
||||
ctx,
|
||||
connLog,
|
||||
config,
|
||||
edgeConn,
|
||||
connOptions,
|
||||
controlStream,
|
||||
connIndex,
|
||||
gracefulShutdownC,
|
||||
reconnectCh,
|
||||
); err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
default:
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, config.EdgeTLSConfigs[protocol], addr.TCP)
|
||||
if err != nil {
|
||||
connLog.Err(err).Msg("Unable to establish connection with Cloudflare edge")
|
||||
return err, true
|
||||
}
|
||||
|
||||
if err := ServeH2mux(
|
||||
ctx,
|
||||
connLog,
|
||||
credentialManager,
|
||||
config,
|
||||
edgeConn,
|
||||
connIndex,
|
||||
connectedFuse,
|
||||
cloudflaredUUID,
|
||||
reconnectCh,
|
||||
gracefulShutdownC,
|
||||
); err != nil {
|
||||
return err, false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type unrecoverableError struct {
|
||||
err error
|
||||
}
|
||||
@@ -403,21 +471,20 @@ func ServeHTTP2(
|
||||
config *TunnelConfig,
|
||||
tlsServerConn net.Conn,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler connection.ControlStreamHandler,
|
||||
connIndex uint8,
|
||||
connectedFuse connection.ConnectedFuse,
|
||||
reconnectCh chan ReconnectSignal,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
reconnectCh chan ReconnectSignal,
|
||||
) error {
|
||||
connLog.Debug().Msgf("Connecting via http2")
|
||||
h2conn := connection.NewHTTP2Connection(
|
||||
tlsServerConn,
|
||||
config.ConnectionConfig,
|
||||
config.NamedTunnel,
|
||||
connOptions,
|
||||
config.Observer,
|
||||
connIndex,
|
||||
connectedFuse,
|
||||
gracefulShutdownC,
|
||||
controlStreamHandler,
|
||||
config.Log,
|
||||
)
|
||||
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
@@ -437,6 +504,65 @@ func ServeHTTP2(
|
||||
return errGroup.Wait()
|
||||
}
|
||||
|
||||
func ServeQUIC(
|
||||
ctx context.Context,
|
||||
edgeAddr *net.UDPAddr,
|
||||
config *TunnelConfig,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler connection.ControlStreamHandler,
|
||||
connIndex uint8,
|
||||
reconnectCh chan ReconnectSignal,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) (err error, recoverable bool) {
|
||||
tlsConfig := config.EdgeTLSConfigs[connection.QUIC]
|
||||
quicConfig := &quic.Config{
|
||||
HandshakeIdleTimeout: quicHandshakeIdleTimeout,
|
||||
MaxIdleTimeout: quicMaxIdleTimeout,
|
||||
MaxIncomingStreams: connection.MaxConcurrentStreams,
|
||||
MaxIncomingUniStreams: connection.MaxConcurrentStreams,
|
||||
KeepAlive: true,
|
||||
Tracer: quicpogs.NewClientTracer(config.Log, connIndex),
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
quicConn, err := connection.NewQUICConnection(
|
||||
ctx,
|
||||
quicConfig,
|
||||
edgeAddr,
|
||||
tlsConfig,
|
||||
config.ConnectionConfig.OriginProxy,
|
||||
connOptions,
|
||||
controlStreamHandler,
|
||||
config.Observer)
|
||||
if err != nil {
|
||||
config.Log.Error().Msgf("Failed to create new quic connection, err: %v", err)
|
||||
return err, true
|
||||
}
|
||||
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
err := quicConn.Serve(ctx)
|
||||
if err != nil {
|
||||
config.Log.Error().Msgf("Failed to serve quic connection, err: %v", err)
|
||||
}
|
||||
return fmt.Errorf("Connection with edge closed")
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return listenReconnect(serveCtx, reconnectCh, gracefulShutdownC)
|
||||
})
|
||||
|
||||
err = errGroup.Wait()
|
||||
if err == nil {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func listenReconnect(ctx context.Context, reconnectCh <-chan ReconnectSignal, gracefulShutdownCh <-chan struct{}) error {
|
||||
select {
|
||||
case reconnect := <-reconnectCh:
|
||||
|
||||
@@ -8,20 +8,18 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
)
|
||||
|
||||
type dynamicMockFetcher struct {
|
||||
percentage int32
|
||||
err error
|
||||
protocolPercents edgediscovery.ProtocolPercents
|
||||
err error
|
||||
}
|
||||
|
||||
func (dmf *dynamicMockFetcher) fetch() connection.PercentageFetcher {
|
||||
return func() (int32, error) {
|
||||
if dmf.err != nil {
|
||||
return 0, dmf.err
|
||||
}
|
||||
return dmf.percentage, nil
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
return dmf.protocolPercents, dmf.err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +37,7 @@ func TestWaitForBackoffFallback(t *testing.T) {
|
||||
},
|
||||
}
|
||||
mockFetcher := dynamicMockFetcher{
|
||||
percentage: 0,
|
||||
protocolPercents: edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}},
|
||||
}
|
||||
warpRoutingEnabled := false
|
||||
protocolSelector, err := connection.NewProtocolSelector(
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go/logging"
|
||||
)
|
||||
|
||||
func perspectiveString(p logging.Perspective) string {
|
||||
switch p {
|
||||
case logging.PerspectiveClient:
|
||||
return "client"
|
||||
case logging.PerspectiveServer:
|
||||
return "server"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert logging.ByteCount(alias for int64) to float64 used in prometheus
|
||||
func byteCountToPromCount(count logging.ByteCount) float64 {
|
||||
return float64(count)
|
||||
}
|
||||
|
||||
// Helper to convert Duration to float64 used in prometheus
|
||||
func durationToPromGauge(duration time.Duration) float64 {
|
||||
return float64(duration.Milliseconds())
|
||||
}
|
||||
|
||||
// Helper to convert https://pkg.go.dev/github.com/lucas-clemente/quic-go@v0.23.0/logging#PacketType into string
|
||||
func packetTypeString(pt logging.PacketType) string {
|
||||
switch pt {
|
||||
case logging.PacketTypeInitial:
|
||||
return "initial"
|
||||
case logging.PacketTypeHandshake:
|
||||
return "handshake"
|
||||
case logging.PacketTypeRetry:
|
||||
return "retry"
|
||||
case logging.PacketType0RTT:
|
||||
return "0_rtt"
|
||||
case logging.PacketTypeVersionNegotiation:
|
||||
return "version_negotiation"
|
||||
case logging.PacketType1RTT:
|
||||
return "1_rtt"
|
||||
case logging.PacketTypeStatelessReset:
|
||||
return "stateless_reset"
|
||||
case logging.PacketTypeNotDetermined:
|
||||
return "undetermined"
|
||||
default:
|
||||
return "unknown_packet_type"
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert https://pkg.go.dev/github.com/lucas-clemente/quic-go@v0.23.0/logging#PacketDropReason into string
|
||||
func packetDropReasonString(reason logging.PacketDropReason) string {
|
||||
switch reason {
|
||||
case logging.PacketDropKeyUnavailable:
|
||||
return "key_unavailable"
|
||||
case logging.PacketDropUnknownConnectionID:
|
||||
return "unknown_conn_id"
|
||||
case logging.PacketDropHeaderParseError:
|
||||
return "header_parse_err"
|
||||
case logging.PacketDropPayloadDecryptError:
|
||||
return "payload_decrypt_err"
|
||||
case logging.PacketDropProtocolViolation:
|
||||
return "protocol_violation"
|
||||
case logging.PacketDropDOSPrevention:
|
||||
return "dos_prevention"
|
||||
case logging.PacketDropUnsupportedVersion:
|
||||
return "unsupported_version"
|
||||
case logging.PacketDropUnexpectedPacket:
|
||||
return "unexpected_packet"
|
||||
case logging.PacketDropUnexpectedSourceConnectionID:
|
||||
return "unexpected_src_conn_id"
|
||||
case logging.PacketDropUnexpectedVersion:
|
||||
return "unexpected_version"
|
||||
case logging.PacketDropDuplicate:
|
||||
return "duplicate"
|
||||
default:
|
||||
return "unknown_reason"
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert https://pkg.go.dev/github.com/lucas-clemente/quic-go@v0.23.0/logging#PacketLossReason into string
|
||||
func packetLossReasonString(reason logging.PacketLossReason) string {
|
||||
switch reason {
|
||||
case logging.PacketLossReorderingThreshold:
|
||||
return "reordering"
|
||||
case logging.PacketLossTimeThreshold:
|
||||
return "timeout"
|
||||
default:
|
||||
return "unknown_loss_reason"
|
||||
}
|
||||
}
|
||||
|
||||
func uint8ToString(input uint8) string {
|
||||
return strconv.FormatUint(uint64(input), 10)
|
||||
}
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/lucas-clemente/quic-go/logging"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "quic"
|
||||
)
|
||||
|
||||
var (
|
||||
clientConnLabels = []string{"conn_index"}
|
||||
clientMetrics = struct {
|
||||
totalConnections prometheus.Counter
|
||||
closedConnections *prometheus.CounterVec
|
||||
sentPackets *prometheus.CounterVec
|
||||
sentBytes *prometheus.CounterVec
|
||||
receivePackets *prometheus.CounterVec
|
||||
receiveBytes *prometheus.CounterVec
|
||||
bufferedPackets *prometheus.CounterVec
|
||||
droppedPackets *prometheus.CounterVec
|
||||
lostPackets *prometheus.CounterVec
|
||||
minRTT *prometheus.GaugeVec
|
||||
latestRTT *prometheus.GaugeVec
|
||||
smoothedRTT *prometheus.GaugeVec
|
||||
}{
|
||||
totalConnections: prometheus.NewCounter(
|
||||
totalConnectionsOpts(logging.PerspectiveClient),
|
||||
),
|
||||
closedConnections: prometheus.NewCounterVec(
|
||||
closedConnectionsOpts(logging.PerspectiveClient),
|
||||
[]string{"error"},
|
||||
),
|
||||
sentPackets: prometheus.NewCounterVec(
|
||||
sentPacketsOpts(logging.PerspectiveClient),
|
||||
clientConnLabels,
|
||||
),
|
||||
sentBytes: prometheus.NewCounterVec(
|
||||
sentBytesOpts(logging.PerspectiveClient),
|
||||
clientConnLabels,
|
||||
),
|
||||
receivePackets: prometheus.NewCounterVec(
|
||||
receivePacketsOpts(logging.PerspectiveClient),
|
||||
clientConnLabels,
|
||||
),
|
||||
receiveBytes: prometheus.NewCounterVec(
|
||||
receiveBytesOpts(logging.PerspectiveClient),
|
||||
clientConnLabels,
|
||||
),
|
||||
bufferedPackets: prometheus.NewCounterVec(
|
||||
bufferedPacketsOpts(logging.PerspectiveClient),
|
||||
append(clientConnLabels, "packet_type"),
|
||||
),
|
||||
droppedPackets: prometheus.NewCounterVec(
|
||||
droppedPacketsOpts(logging.PerspectiveClient),
|
||||
append(clientConnLabels, "packet_type", "reason"),
|
||||
),
|
||||
lostPackets: prometheus.NewCounterVec(
|
||||
lostPacketsOpts(logging.PerspectiveClient),
|
||||
append(clientConnLabels, "reason"),
|
||||
),
|
||||
minRTT: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(logging.PerspectiveClient),
|
||||
Name: "min_rtt",
|
||||
Help: "Lowest RTT measured on a connection in millisec",
|
||||
},
|
||||
clientConnLabels,
|
||||
),
|
||||
latestRTT: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(logging.PerspectiveClient),
|
||||
Name: "latest_rtt",
|
||||
Help: "Latest RTT measured on a connection",
|
||||
},
|
||||
clientConnLabels,
|
||||
),
|
||||
smoothedRTT: prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(logging.PerspectiveClient),
|
||||
Name: "smoothed_rtt",
|
||||
Help: "Calculated smoothed RTT measured on a connection in millisec",
|
||||
},
|
||||
clientConnLabels,
|
||||
),
|
||||
}
|
||||
// The server has many QUIC connections. Adding per connection label incurs high memory cost
|
||||
serverMetrics = struct {
|
||||
totalConnections prometheus.Counter
|
||||
closedConnections prometheus.Counter
|
||||
sentPackets prometheus.Counter
|
||||
sentBytes prometheus.Counter
|
||||
receivePackets prometheus.Counter
|
||||
receiveBytes prometheus.Counter
|
||||
bufferedPackets *prometheus.CounterVec
|
||||
droppedPackets *prometheus.CounterVec
|
||||
lostPackets *prometheus.CounterVec
|
||||
rtt prometheus.Histogram
|
||||
}{
|
||||
totalConnections: prometheus.NewCounter(
|
||||
totalConnectionsOpts(logging.PerspectiveServer),
|
||||
),
|
||||
closedConnections: prometheus.NewCounter(
|
||||
closedConnectionsOpts(logging.PerspectiveServer),
|
||||
),
|
||||
sentPackets: prometheus.NewCounter(
|
||||
sentPacketsOpts(logging.PerspectiveServer),
|
||||
),
|
||||
sentBytes: prometheus.NewCounter(
|
||||
sentBytesOpts(logging.PerspectiveServer),
|
||||
),
|
||||
receivePackets: prometheus.NewCounter(
|
||||
receivePacketsOpts(logging.PerspectiveServer),
|
||||
),
|
||||
receiveBytes: prometheus.NewCounter(
|
||||
receiveBytesOpts(logging.PerspectiveServer),
|
||||
),
|
||||
bufferedPackets: prometheus.NewCounterVec(
|
||||
bufferedPacketsOpts(logging.PerspectiveServer),
|
||||
[]string{"packet_type"},
|
||||
),
|
||||
droppedPackets: prometheus.NewCounterVec(
|
||||
droppedPacketsOpts(logging.PerspectiveServer),
|
||||
[]string{"packet_type", "reason"},
|
||||
),
|
||||
lostPackets: prometheus.NewCounterVec(
|
||||
lostPacketsOpts(logging.PerspectiveServer),
|
||||
[]string{"reason"},
|
||||
),
|
||||
rtt: prometheus.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(logging.PerspectiveServer),
|
||||
Name: "rtt",
|
||||
Buckets: []float64{5, 10, 20, 30, 40, 50, 75, 100},
|
||||
},
|
||||
),
|
||||
}
|
||||
registerClient = sync.Once{}
|
||||
registerServer = sync.Once{}
|
||||
)
|
||||
|
||||
// MetricsCollector abstracts the difference between client and server metrics from connTracer
|
||||
type MetricsCollector interface {
|
||||
startedConnection()
|
||||
closedConnection(err error)
|
||||
sentPackets(logging.ByteCount)
|
||||
receivedPackets(logging.ByteCount)
|
||||
bufferedPackets(logging.PacketType)
|
||||
droppedPackets(logging.PacketType, logging.ByteCount, logging.PacketDropReason)
|
||||
lostPackets(logging.PacketLossReason)
|
||||
updatedRTT(*logging.RTTStats)
|
||||
}
|
||||
|
||||
func totalConnectionsOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
var help string
|
||||
if p == logging.PerspectiveClient {
|
||||
help = "Number of connections initiated. For all quic metrics, client means the side initiating the connection"
|
||||
} else {
|
||||
help = "Number of connections accepted. For all quic metrics, server means the side accepting connections"
|
||||
}
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "total_connections",
|
||||
Help: help,
|
||||
}
|
||||
}
|
||||
|
||||
func closedConnectionsOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "closed_connections",
|
||||
Help: "Number of connections that has been closed",
|
||||
}
|
||||
}
|
||||
|
||||
func sentPacketsOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "sent_packets",
|
||||
Help: "Number of packets that have been sent through a connection",
|
||||
}
|
||||
}
|
||||
|
||||
func sentBytesOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "sent_bytes",
|
||||
Help: "Number of bytes that have been sent through a connection",
|
||||
}
|
||||
}
|
||||
|
||||
func receivePacketsOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "receive_packets",
|
||||
Help: "Number of packets that have been received through a connection",
|
||||
}
|
||||
}
|
||||
|
||||
func receiveBytesOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "receive_bytes",
|
||||
Help: "Number of bytes that have been received through a connection",
|
||||
}
|
||||
}
|
||||
|
||||
func bufferedPacketsOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "buffered_packets",
|
||||
Help: "Number of bytes that have been buffered on a connection",
|
||||
}
|
||||
}
|
||||
|
||||
func droppedPacketsOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "dropped_packets",
|
||||
Help: "Number of bytes that have been dropped on a connection",
|
||||
}
|
||||
}
|
||||
|
||||
func lostPacketsOpts(p logging.Perspective) prometheus.CounterOpts {
|
||||
return prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: perspectiveString(p),
|
||||
Name: "lost_packets",
|
||||
Help: "Number of packets that have been lost from a connection",
|
||||
}
|
||||
}
|
||||
|
||||
type clientCollector struct {
|
||||
index string
|
||||
}
|
||||
|
||||
func newClientCollector(index uint8) MetricsCollector {
|
||||
registerClient.Do(func() {
|
||||
prometheus.MustRegister(
|
||||
clientMetrics.totalConnections,
|
||||
clientMetrics.closedConnections,
|
||||
clientMetrics.sentPackets,
|
||||
clientMetrics.sentBytes,
|
||||
clientMetrics.receivePackets,
|
||||
clientMetrics.receiveBytes,
|
||||
clientMetrics.bufferedPackets,
|
||||
clientMetrics.droppedPackets,
|
||||
clientMetrics.lostPackets,
|
||||
clientMetrics.minRTT,
|
||||
clientMetrics.latestRTT,
|
||||
clientMetrics.smoothedRTT,
|
||||
)
|
||||
})
|
||||
return &clientCollector{
|
||||
index: uint8ToString(index),
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *clientCollector) startedConnection() {
|
||||
clientMetrics.totalConnections.Inc()
|
||||
}
|
||||
|
||||
func (cc *clientCollector) closedConnection(err error) {
|
||||
clientMetrics.closedConnections.WithLabelValues(err.Error()).Inc()
|
||||
}
|
||||
|
||||
func (cc *clientCollector) sentPackets(size logging.ByteCount) {
|
||||
clientMetrics.sentPackets.WithLabelValues(cc.index).Inc()
|
||||
clientMetrics.sentBytes.WithLabelValues(cc.index).Add(byteCountToPromCount(size))
|
||||
}
|
||||
|
||||
func (cc *clientCollector) receivedPackets(size logging.ByteCount) {
|
||||
clientMetrics.receivePackets.WithLabelValues(cc.index).Inc()
|
||||
clientMetrics.receiveBytes.WithLabelValues(cc.index).Add(byteCountToPromCount(size))
|
||||
}
|
||||
|
||||
func (cc *clientCollector) bufferedPackets(packetType logging.PacketType) {
|
||||
clientMetrics.bufferedPackets.WithLabelValues(cc.index, packetTypeString(packetType)).Inc()
|
||||
}
|
||||
|
||||
func (cc *clientCollector) droppedPackets(packetType logging.PacketType, size logging.ByteCount, reason logging.PacketDropReason) {
|
||||
clientMetrics.droppedPackets.WithLabelValues(
|
||||
cc.index,
|
||||
packetTypeString(packetType),
|
||||
packetDropReasonString(reason),
|
||||
).Add(byteCountToPromCount(size))
|
||||
}
|
||||
|
||||
func (cc *clientCollector) lostPackets(reason logging.PacketLossReason) {
|
||||
clientMetrics.lostPackets.WithLabelValues(cc.index, packetLossReasonString(reason)).Inc()
|
||||
}
|
||||
|
||||
func (cc *clientCollector) updatedRTT(rtt *logging.RTTStats) {
|
||||
clientMetrics.minRTT.WithLabelValues(cc.index).Set(durationToPromGauge(rtt.MinRTT()))
|
||||
clientMetrics.latestRTT.WithLabelValues(cc.index).Set(durationToPromGauge(rtt.LatestRTT()))
|
||||
clientMetrics.smoothedRTT.WithLabelValues(cc.index).Set(durationToPromGauge(rtt.SmoothedRTT()))
|
||||
}
|
||||
|
||||
type serverCollector struct{}
|
||||
|
||||
func newServiceCollector() MetricsCollector {
|
||||
registerServer.Do(func() {
|
||||
prometheus.MustRegister(
|
||||
serverMetrics.totalConnections,
|
||||
serverMetrics.closedConnections,
|
||||
serverMetrics.sentPackets,
|
||||
serverMetrics.sentBytes,
|
||||
serverMetrics.receivePackets,
|
||||
serverMetrics.receiveBytes,
|
||||
serverMetrics.bufferedPackets,
|
||||
serverMetrics.droppedPackets,
|
||||
serverMetrics.lostPackets,
|
||||
serverMetrics.rtt,
|
||||
)
|
||||
})
|
||||
return &serverCollector{}
|
||||
}
|
||||
|
||||
func (sc *serverCollector) startedConnection() {
|
||||
serverMetrics.totalConnections.Inc()
|
||||
}
|
||||
|
||||
func (sc *serverCollector) closedConnection(err error) {
|
||||
serverMetrics.closedConnections.Inc()
|
||||
}
|
||||
|
||||
func (sc *serverCollector) sentPackets(size logging.ByteCount) {
|
||||
serverMetrics.sentPackets.Inc()
|
||||
serverMetrics.sentBytes.Add(byteCountToPromCount(size))
|
||||
}
|
||||
|
||||
func (sc *serverCollector) receivedPackets(size logging.ByteCount) {
|
||||
serverMetrics.receivePackets.Inc()
|
||||
serverMetrics.receiveBytes.Add(byteCountToPromCount(size))
|
||||
}
|
||||
|
||||
func (sc *serverCollector) bufferedPackets(packetType logging.PacketType) {
|
||||
serverMetrics.bufferedPackets.WithLabelValues(packetTypeString(packetType)).Inc()
|
||||
}
|
||||
|
||||
func (sc *serverCollector) droppedPackets(packetType logging.PacketType, size logging.ByteCount, reason logging.PacketDropReason) {
|
||||
serverMetrics.droppedPackets.WithLabelValues(
|
||||
packetTypeString(packetType),
|
||||
packetDropReasonString(reason),
|
||||
).Add(byteCountToPromCount(size))
|
||||
}
|
||||
|
||||
func (sc *serverCollector) lostPackets(reason logging.PacketLossReason) {
|
||||
serverMetrics.lostPackets.WithLabelValues(packetLossReasonString(reason)).Inc()
|
||||
}
|
||||
|
||||
func (sc *serverCollector) updatedRTT(rtt *logging.RTTStats) {
|
||||
latestRTT := rtt.LatestRTT()
|
||||
// May return 0 if no valid updates have occured
|
||||
if latestRTT > 0 {
|
||||
serverMetrics.rtt.Observe(durationToPromGauge(latestRTT))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
"zombiezen.com/go/capnproto2/pogs"
|
||||
|
||||
"github.com/cloudflare/cloudflared/quic/schema"
|
||||
)
|
||||
|
||||
// ConnectionType indicates the type of underlying connection proxied within the QUIC stream.
|
||||
type ConnectionType uint16
|
||||
|
||||
const (
|
||||
ConnectionTypeHTTP ConnectionType = iota
|
||||
ConnectionTypeWebsocket
|
||||
ConnectionTypeTCP
|
||||
)
|
||||
|
||||
// ConnectRequest is the representation of metadata sent at the start of a QUIC application handshake.
|
||||
type ConnectRequest struct {
|
||||
Dest string `capnp:"dest"`
|
||||
Type ConnectionType `capnp:"type"`
|
||||
Metadata []Metadata `capnp:"metadata"`
|
||||
}
|
||||
|
||||
// Metadata is a representation of key value based data sent via RequestMeta.
|
||||
type Metadata struct {
|
||||
Key string `capnp:"key"`
|
||||
Val string `capnp:"val"`
|
||||
}
|
||||
|
||||
// MetadataMap returns a map format of []Metadata.
|
||||
func (r *ConnectRequest) MetadataMap() map[string]string {
|
||||
metadataMap := make(map[string]string)
|
||||
for _, metadata := range r.Metadata {
|
||||
metadataMap[metadata.Key] = metadata.Val
|
||||
}
|
||||
return metadataMap
|
||||
}
|
||||
|
||||
func (r *ConnectRequest) fromPogs(msg *capnp.Message) error {
|
||||
metadata, err := schema.ReadRootConnectRequest(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pogs.Extract(r, schema.ConnectRequest_TypeID, metadata.Struct)
|
||||
}
|
||||
|
||||
func (r *ConnectRequest) toPogs() (*capnp.Message, error) {
|
||||
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root, err := schema.NewRootConnectRequest(seg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := pogs.Insert(schema.ConnectRequest_TypeID, root.Struct, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// ConnectResponse is a representation of metadata sent as a response to a QUIC application handshake.
|
||||
type ConnectResponse struct {
|
||||
Error string `capnp:"error"`
|
||||
Metadata []Metadata `capnp:"metadata"`
|
||||
}
|
||||
|
||||
func (r *ConnectResponse) fromPogs(msg *capnp.Message) error {
|
||||
metadata, err := schema.ReadRootConnectResponse(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pogs.Extract(r, schema.ConnectResponse_TypeID, metadata.Struct)
|
||||
}
|
||||
|
||||
func (r *ConnectResponse) toPogs() (*capnp.Message, error) {
|
||||
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root, err := schema.NewRootConnectResponse(seg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := pogs.Insert(schema.ConnectResponse_TypeID, root.Struct, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
)
|
||||
|
||||
// protocolSignature is a custom protocol signature to ensure that whoever performs a handshake does not write data
|
||||
// before writing the metadata.
|
||||
var protocolSignature = []byte{0x0A, 0x36, 0xCD, 0x12, 0xA1, 0x3E}
|
||||
|
||||
const protocolVersionLength = 2
|
||||
|
||||
type protocolVersion string
|
||||
|
||||
const (
|
||||
protocolV1 protocolVersion = "01"
|
||||
)
|
||||
|
||||
// ReadConnectRequestData reads the handshake data from a QUIC stream.
|
||||
func ReadConnectRequestData(stream io.Reader) (*ConnectRequest, error) {
|
||||
if err := verifySignature(stream); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
|
||||
if _, err := readVersion(stream); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg, err := capnp.NewDecoder(stream).Decode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &ConnectRequest{}
|
||||
if err := r.fromPogs(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// WriteConnectRequestData writes requestMeta to a stream.
|
||||
func WriteConnectRequestData(stream io.Writer, dest string, connectionType ConnectionType, metadata ...Metadata) error {
|
||||
connectRequest := &ConnectRequest{
|
||||
Dest: dest,
|
||||
Type: connectionType,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
msg, err := connectRequest.toPogs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writePreamble(stream); err != nil {
|
||||
return err
|
||||
}
|
||||
return capnp.NewEncoder(stream).Encode(msg)
|
||||
}
|
||||
|
||||
// ReadConnectResponseData reads the response to a RequestMeta in a stream.
|
||||
func ReadConnectResponseData(stream io.Reader) (*ConnectResponse, error) {
|
||||
if err := verifySignature(stream); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
|
||||
if _, err := readVersion(stream); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg, err := capnp.NewDecoder(stream).Decode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &ConnectResponse{}
|
||||
if err := r.fromPogs(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// WriteConnectResponseData writes response to a QUIC stream.
|
||||
func WriteConnectResponseData(stream io.Writer, respErr error, metadata ...Metadata) error {
|
||||
var connectResponse *ConnectResponse
|
||||
if respErr != nil {
|
||||
connectResponse = &ConnectResponse{
|
||||
Error: respErr.Error(),
|
||||
}
|
||||
} else {
|
||||
connectResponse = &ConnectResponse{
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := connectResponse.toPogs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := writePreamble(stream); err != nil {
|
||||
return err
|
||||
}
|
||||
return capnp.NewEncoder(stream).Encode(msg)
|
||||
}
|
||||
|
||||
func writePreamble(stream io.Writer) error {
|
||||
if err := writeSignature(stream); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeVersion(stream)
|
||||
}
|
||||
|
||||
func writeVersion(stream io.Writer) error {
|
||||
_, err := stream.Write([]byte(protocolV1)[:protocolVersionLength])
|
||||
return err
|
||||
}
|
||||
|
||||
func readVersion(stream io.Reader) (string, error) {
|
||||
version := make([]byte, protocolVersionLength)
|
||||
_, err := stream.Read(version)
|
||||
return string(version), err
|
||||
}
|
||||
|
||||
func writeSignature(stream io.Writer) error {
|
||||
_, err := stream.Write(protocolSignature)
|
||||
return err
|
||||
}
|
||||
|
||||
func verifySignature(stream io.Reader) error {
|
||||
signature := make([]byte, len(protocolSignature))
|
||||
if _, err := io.ReadFull(stream, signature); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(signature[0:], protocolSignature) {
|
||||
return fmt.Errorf("Wrong signature: %v", signature)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConnectRequestData(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
hostname string
|
||||
connectionType ConnectionType
|
||||
metadata []Metadata
|
||||
}{
|
||||
{
|
||||
name: "Signature verified and request metadata is unmarshaled and read correctly",
|
||||
hostname: "tunnel.com",
|
||||
connectionType: ConnectionTypeHTTP,
|
||||
metadata: []Metadata{
|
||||
Metadata{
|
||||
Key: "key",
|
||||
Val: "1234",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b := &bytes.Buffer{}
|
||||
err := WriteConnectRequestData(b, test.hostname, test.connectionType, test.metadata...)
|
||||
require.NoError(t, err)
|
||||
reqMeta, err := ReadConnectRequestData(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.metadata, reqMeta.Metadata)
|
||||
assert.Equal(t, test.hostname, reqMeta.Dest)
|
||||
assert.Equal(t, test.connectionType, reqMeta.Type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectResponseMeta(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
err error
|
||||
metadata []Metadata
|
||||
}{
|
||||
{
|
||||
name: "Signature verified and response metadata is unmarshaled and read correctly",
|
||||
metadata: []Metadata{
|
||||
Metadata{
|
||||
Key: "key",
|
||||
Val: "1234",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "If error is not empty, other fields should be blank",
|
||||
err: errors.New("something happened"),
|
||||
metadata: []Metadata{
|
||||
Metadata{
|
||||
Key: "key",
|
||||
Val: "1234",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b := &bytes.Buffer{}
|
||||
err := WriteConnectResponseData(b, test.err, test.metadata...)
|
||||
require.NoError(t, err)
|
||||
respMeta, err := ReadConnectResponseData(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
if respMeta.Error == "" {
|
||||
assert.Equal(t, test.metadata, respMeta.Metadata)
|
||||
} else {
|
||||
assert.Equal(t, 0, len(respMeta.Metadata))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Go = import "/go.capnp";
|
||||
@0xb29021ef7421cc32;
|
||||
|
||||
$Go.package("schema");
|
||||
$Go.import("schema");
|
||||
|
||||
|
||||
struct ConnectRequest{
|
||||
dest @0 :Text;
|
||||
type @1 :ConnectionType;
|
||||
metadata @2 :List(Metadata);
|
||||
}
|
||||
|
||||
enum ConnectionType{
|
||||
http @0;
|
||||
websocket @1;
|
||||
tcp @2;
|
||||
}
|
||||
|
||||
struct Metadata {
|
||||
key @0 :Text;
|
||||
val @1 :Text;
|
||||
}
|
||||
|
||||
struct ConnectResponse{
|
||||
error @0 :Text;
|
||||
metadata @1 :List(Metadata);
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
// Code generated by capnpc-go. DO NOT EDIT.
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
capnp "zombiezen.com/go/capnproto2"
|
||||
text "zombiezen.com/go/capnproto2/encoding/text"
|
||||
schemas "zombiezen.com/go/capnproto2/schemas"
|
||||
)
|
||||
|
||||
type ConnectRequest struct{ capnp.Struct }
|
||||
|
||||
// ConnectRequest_TypeID is the unique identifier for the type ConnectRequest.
|
||||
const ConnectRequest_TypeID = 0xc47116a1045e4061
|
||||
|
||||
func NewConnectRequest(s *capnp.Segment) (ConnectRequest, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2})
|
||||
return ConnectRequest{st}, err
|
||||
}
|
||||
|
||||
func NewRootConnectRequest(s *capnp.Segment) (ConnectRequest, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2})
|
||||
return ConnectRequest{st}, err
|
||||
}
|
||||
|
||||
func ReadRootConnectRequest(msg *capnp.Message) (ConnectRequest, error) {
|
||||
root, err := msg.RootPtr()
|
||||
return ConnectRequest{root.Struct()}, err
|
||||
}
|
||||
|
||||
func (s ConnectRequest) String() string {
|
||||
str, _ := text.Marshal(0xc47116a1045e4061, s.Struct)
|
||||
return str
|
||||
}
|
||||
|
||||
func (s ConnectRequest) Dest() (string, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.Text(), err
|
||||
}
|
||||
|
||||
func (s ConnectRequest) HasDest() bool {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s ConnectRequest) DestBytes() ([]byte, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.TextBytes(), err
|
||||
}
|
||||
|
||||
func (s ConnectRequest) SetDest(v string) error {
|
||||
return s.Struct.SetText(0, v)
|
||||
}
|
||||
|
||||
func (s ConnectRequest) Type() ConnectionType {
|
||||
return ConnectionType(s.Struct.Uint16(0))
|
||||
}
|
||||
|
||||
func (s ConnectRequest) SetType(v ConnectionType) {
|
||||
s.Struct.SetUint16(0, uint16(v))
|
||||
}
|
||||
|
||||
func (s ConnectRequest) Metadata() (Metadata_List, error) {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return Metadata_List{List: p.List()}, err
|
||||
}
|
||||
|
||||
func (s ConnectRequest) HasMetadata() bool {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s ConnectRequest) SetMetadata(v Metadata_List) error {
|
||||
return s.Struct.SetPtr(1, v.List.ToPtr())
|
||||
}
|
||||
|
||||
// NewMetadata sets the metadata field to a newly
|
||||
// allocated Metadata_List, preferring placement in s's segment.
|
||||
func (s ConnectRequest) NewMetadata(n int32) (Metadata_List, error) {
|
||||
l, err := NewMetadata_List(s.Struct.Segment(), n)
|
||||
if err != nil {
|
||||
return Metadata_List{}, err
|
||||
}
|
||||
err = s.Struct.SetPtr(1, l.List.ToPtr())
|
||||
return l, err
|
||||
}
|
||||
|
||||
// ConnectRequest_List is a list of ConnectRequest.
|
||||
type ConnectRequest_List struct{ capnp.List }
|
||||
|
||||
// NewConnectRequest creates a new list of ConnectRequest.
|
||||
func NewConnectRequest_List(s *capnp.Segment, sz int32) (ConnectRequest_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}, sz)
|
||||
return ConnectRequest_List{l}, err
|
||||
}
|
||||
|
||||
func (s ConnectRequest_List) At(i int) ConnectRequest { return ConnectRequest{s.List.Struct(i)} }
|
||||
|
||||
func (s ConnectRequest_List) Set(i int, v ConnectRequest) error { return s.List.SetStruct(i, v.Struct) }
|
||||
|
||||
func (s ConnectRequest_List) String() string {
|
||||
str, _ := text.MarshalList(0xc47116a1045e4061, s.List)
|
||||
return str
|
||||
}
|
||||
|
||||
// ConnectRequest_Promise is a wrapper for a ConnectRequest promised by a client call.
|
||||
type ConnectRequest_Promise struct{ *capnp.Pipeline }
|
||||
|
||||
func (p ConnectRequest_Promise) Struct() (ConnectRequest, error) {
|
||||
s, err := p.Pipeline.Struct()
|
||||
return ConnectRequest{s}, err
|
||||
}
|
||||
|
||||
type ConnectionType uint16
|
||||
|
||||
// ConnectionType_TypeID is the unique identifier for the type ConnectionType.
|
||||
const ConnectionType_TypeID = 0xc52e1bac26d379c8
|
||||
|
||||
// Values of ConnectionType.
|
||||
const (
|
||||
ConnectionType_http ConnectionType = 0
|
||||
ConnectionType_websocket ConnectionType = 1
|
||||
ConnectionType_tcp ConnectionType = 2
|
||||
)
|
||||
|
||||
// String returns the enum's constant name.
|
||||
func (c ConnectionType) String() string {
|
||||
switch c {
|
||||
case ConnectionType_http:
|
||||
return "http"
|
||||
case ConnectionType_websocket:
|
||||
return "websocket"
|
||||
case ConnectionType_tcp:
|
||||
return "tcp"
|
||||
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectionTypeFromString returns the enum value with a name,
|
||||
// or the zero value if there's no such value.
|
||||
func ConnectionTypeFromString(c string) ConnectionType {
|
||||
switch c {
|
||||
case "http":
|
||||
return ConnectionType_http
|
||||
case "websocket":
|
||||
return ConnectionType_websocket
|
||||
case "tcp":
|
||||
return ConnectionType_tcp
|
||||
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectionType_List struct{ capnp.List }
|
||||
|
||||
func NewConnectionType_List(s *capnp.Segment, sz int32) (ConnectionType_List, error) {
|
||||
l, err := capnp.NewUInt16List(s, sz)
|
||||
return ConnectionType_List{l.List}, err
|
||||
}
|
||||
|
||||
func (l ConnectionType_List) At(i int) ConnectionType {
|
||||
ul := capnp.UInt16List{List: l.List}
|
||||
return ConnectionType(ul.At(i))
|
||||
}
|
||||
|
||||
func (l ConnectionType_List) Set(i int, v ConnectionType) {
|
||||
ul := capnp.UInt16List{List: l.List}
|
||||
ul.Set(i, uint16(v))
|
||||
}
|
||||
|
||||
type Metadata struct{ capnp.Struct }
|
||||
|
||||
// Metadata_TypeID is the unique identifier for the type Metadata.
|
||||
const Metadata_TypeID = 0xe1446b97bfd1cd37
|
||||
|
||||
func NewMetadata(s *capnp.Segment) (Metadata, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return Metadata{st}, err
|
||||
}
|
||||
|
||||
func NewRootMetadata(s *capnp.Segment) (Metadata, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return Metadata{st}, err
|
||||
}
|
||||
|
||||
func ReadRootMetadata(msg *capnp.Message) (Metadata, error) {
|
||||
root, err := msg.RootPtr()
|
||||
return Metadata{root.Struct()}, err
|
||||
}
|
||||
|
||||
func (s Metadata) String() string {
|
||||
str, _ := text.Marshal(0xe1446b97bfd1cd37, s.Struct)
|
||||
return str
|
||||
}
|
||||
|
||||
func (s Metadata) Key() (string, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.Text(), err
|
||||
}
|
||||
|
||||
func (s Metadata) HasKey() bool {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s Metadata) KeyBytes() ([]byte, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.TextBytes(), err
|
||||
}
|
||||
|
||||
func (s Metadata) SetKey(v string) error {
|
||||
return s.Struct.SetText(0, v)
|
||||
}
|
||||
|
||||
func (s Metadata) Val() (string, error) {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.Text(), err
|
||||
}
|
||||
|
||||
func (s Metadata) HasVal() bool {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s Metadata) ValBytes() ([]byte, error) {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.TextBytes(), err
|
||||
}
|
||||
|
||||
func (s Metadata) SetVal(v string) error {
|
||||
return s.Struct.SetText(1, v)
|
||||
}
|
||||
|
||||
// Metadata_List is a list of Metadata.
|
||||
type Metadata_List struct{ capnp.List }
|
||||
|
||||
// NewMetadata creates a new list of Metadata.
|
||||
func NewMetadata_List(s *capnp.Segment, sz int32) (Metadata_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
|
||||
return Metadata_List{l}, err
|
||||
}
|
||||
|
||||
func (s Metadata_List) At(i int) Metadata { return Metadata{s.List.Struct(i)} }
|
||||
|
||||
func (s Metadata_List) Set(i int, v Metadata) error { return s.List.SetStruct(i, v.Struct) }
|
||||
|
||||
func (s Metadata_List) String() string {
|
||||
str, _ := text.MarshalList(0xe1446b97bfd1cd37, s.List)
|
||||
return str
|
||||
}
|
||||
|
||||
// Metadata_Promise is a wrapper for a Metadata promised by a client call.
|
||||
type Metadata_Promise struct{ *capnp.Pipeline }
|
||||
|
||||
func (p Metadata_Promise) Struct() (Metadata, error) {
|
||||
s, err := p.Pipeline.Struct()
|
||||
return Metadata{s}, err
|
||||
}
|
||||
|
||||
type ConnectResponse struct{ capnp.Struct }
|
||||
|
||||
// ConnectResponse_TypeID is the unique identifier for the type ConnectResponse.
|
||||
const ConnectResponse_TypeID = 0xb1032ec91cef8727
|
||||
|
||||
func NewConnectResponse(s *capnp.Segment) (ConnectResponse, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return ConnectResponse{st}, err
|
||||
}
|
||||
|
||||
func NewRootConnectResponse(s *capnp.Segment) (ConnectResponse, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return ConnectResponse{st}, err
|
||||
}
|
||||
|
||||
func ReadRootConnectResponse(msg *capnp.Message) (ConnectResponse, error) {
|
||||
root, err := msg.RootPtr()
|
||||
return ConnectResponse{root.Struct()}, err
|
||||
}
|
||||
|
||||
func (s ConnectResponse) String() string {
|
||||
str, _ := text.Marshal(0xb1032ec91cef8727, s.Struct)
|
||||
return str
|
||||
}
|
||||
|
||||
func (s ConnectResponse) Error() (string, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.Text(), err
|
||||
}
|
||||
|
||||
func (s ConnectResponse) HasError() bool {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s ConnectResponse) ErrorBytes() ([]byte, error) {
|
||||
p, err := s.Struct.Ptr(0)
|
||||
return p.TextBytes(), err
|
||||
}
|
||||
|
||||
func (s ConnectResponse) SetError(v string) error {
|
||||
return s.Struct.SetText(0, v)
|
||||
}
|
||||
|
||||
func (s ConnectResponse) Metadata() (Metadata_List, error) {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return Metadata_List{List: p.List()}, err
|
||||
}
|
||||
|
||||
func (s ConnectResponse) HasMetadata() bool {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s ConnectResponse) SetMetadata(v Metadata_List) error {
|
||||
return s.Struct.SetPtr(1, v.List.ToPtr())
|
||||
}
|
||||
|
||||
// NewMetadata sets the metadata field to a newly
|
||||
// allocated Metadata_List, preferring placement in s's segment.
|
||||
func (s ConnectResponse) NewMetadata(n int32) (Metadata_List, error) {
|
||||
l, err := NewMetadata_List(s.Struct.Segment(), n)
|
||||
if err != nil {
|
||||
return Metadata_List{}, err
|
||||
}
|
||||
err = s.Struct.SetPtr(1, l.List.ToPtr())
|
||||
return l, err
|
||||
}
|
||||
|
||||
// ConnectResponse_List is a list of ConnectResponse.
|
||||
type ConnectResponse_List struct{ capnp.List }
|
||||
|
||||
// NewConnectResponse creates a new list of ConnectResponse.
|
||||
func NewConnectResponse_List(s *capnp.Segment, sz int32) (ConnectResponse_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
|
||||
return ConnectResponse_List{l}, err
|
||||
}
|
||||
|
||||
func (s ConnectResponse_List) At(i int) ConnectResponse { return ConnectResponse{s.List.Struct(i)} }
|
||||
|
||||
func (s ConnectResponse_List) Set(i int, v ConnectResponse) error {
|
||||
return s.List.SetStruct(i, v.Struct)
|
||||
}
|
||||
|
||||
func (s ConnectResponse_List) String() string {
|
||||
str, _ := text.MarshalList(0xb1032ec91cef8727, s.List)
|
||||
return str
|
||||
}
|
||||
|
||||
// ConnectResponse_Promise is a wrapper for a ConnectResponse promised by a client call.
|
||||
type ConnectResponse_Promise struct{ *capnp.Pipeline }
|
||||
|
||||
func (p ConnectResponse_Promise) Struct() (ConnectResponse, error) {
|
||||
s, err := p.Pipeline.Struct()
|
||||
return ConnectResponse{s}, err
|
||||
}
|
||||
|
||||
const schema_b29021ef7421cc32 = "x\xda\xb4\x91\xcfk\x13A\x1c\xc5\xdf\x9bI\xba\x1e\xa2" +
|
||||
"\x9b!\xd5\x8b\x8a\xa4\xf8+ES\xdb(\xa2\xa7\x80\x15" +
|
||||
"TZ\xcc\x14\xcf\x96u;\x98\x92vw\x92\x9dZ\xf2" +
|
||||
"\x17x\x15/\xe2\xd1\xbb \x15<\x0b\xa2\xa0\xa2\x07\x11" +
|
||||
"\xff\x80\xfe\x05=y\xf0\xb42)\xdb@)\x08Bo" +
|
||||
"\xdfy<\xe6}\xbe\xdfW\xfd\xd5\x16\xb3\xe5\xc7\x04t" +
|
||||
"\xb5<\x91_x\xbas\xeaKSnA5\x98\xcf}" +
|
||||
"\xab\xbb\x9d\xfa\xb3\xb7(\x8b\x00\x98}\xf9\x95\xea]\x00" +
|
||||
"\xa8\xadM0\x8f\xda\x0fK\xafN\xf4?B7\xb8\xdf" +
|
||||
"\xda\xaa\xf3\x03k7\x18\x00\xb5k|\x03\xe6\x9f\x87?" +
|
||||
"\xcf\xbf>\xd9\xfc\x04\xd5\x10c3\xd8\xda\xf6\xce?#" +
|
||||
"\xe7o\xde\x07\xf3\xeb\xdf\x7f\xbc\x7f\xd1\x9b\xdf>\x80\xa0" +
|
||||
"uT<g\xed\x9c\x1fku\xe1!\xfa\x1b\xab\xf1L" +
|
||||
"\x16w'\xccz4\xe3\x1f\xcb\xeb\xc6E+\x91\x8b\x96" +
|
||||
"\xed ui\x9c\xae5\xe3\xc8&\xf6\xe6\xad4IL" +
|
||||
"\xec\x96Lf\xd3$3@\x87\xd4Gd\x09(\x11P" +
|
||||
"\x8d9@\x9f\x95\xd4W\x04\x159I/^\xbe\x07\xe8" +
|
||||
"K\x92\xfa\x8e\xe0\x193\x18\xa4\x03V X\x01\xf3\"" +
|
||||
"\x06\x00\x8f\x81\x1dIV\xc7\xe8\xa0\x17\xff\x87\xae\xbfa" +
|
||||
"27b\xab\xec\xb1\xdd\x9e\x06t[R/\x08\x16h" +
|
||||
"w\xbd6/\xa9;\x82Jp\x92\x02P\x8b\x9ewA" +
|
||||
"Rw\x05\xc3\x15\x93\xb9\x027tCk\x18\x8e[\x00" +
|
||||
"\x19\x1e\xda\x16\xabi\xf2`h\xcd\xee\x16#\xb0\xd3\xd3" +
|
||||
"\xfe3u|\x09\xa0Pj\x0a\x08\xbb\xce\xd9|\xd3<" +
|
||||
"\xca\xd2\xb8g@\x17\xb8\xd8\xeeE\x95\xff\x19\xb5\xb8\xab" +
|
||||
"3\xdaW\xe3\xd4A5z\xf1\xa2\xa4\xbe*\x18\xf4\xcc" +
|
||||
"\xb0\xb8J\xf0$Z+\xe6\xbf\x01\x00\x00\xff\xff\xf5\xed" +
|
||||
"\xc9\xfe"
|
||||
|
||||
func init() {
|
||||
schemas.Register(schema_b29021ef7421cc32,
|
||||
0xb1032ec91cef8727,
|
||||
0xc47116a1045e4061,
|
||||
0xc52e1bac26d379c8,
|
||||
0xe1446b97bfd1cd37)
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/lucas-clemente/quic-go/logging"
|
||||
"github.com/lucas-clemente/quic-go/qlog"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// QUICTracer is a wrapper to create new quicConnTracer
|
||||
type tracer struct {
|
||||
logger *zerolog.Logger
|
||||
config *tracerConfig
|
||||
}
|
||||
|
||||
type tracerConfig struct {
|
||||
isClient bool
|
||||
// Only client has an index
|
||||
index uint8
|
||||
}
|
||||
|
||||
func NewClientTracer(logger *zerolog.Logger, index uint8) logging.Tracer {
|
||||
return &tracer{
|
||||
logger: logger,
|
||||
config: &tracerConfig{
|
||||
isClient: true,
|
||||
index: index,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewServerTracer(logger *zerolog.Logger) logging.Tracer {
|
||||
return &tracer{
|
||||
logger: logger,
|
||||
config: &tracerConfig{
|
||||
isClient: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tracer) TracerForConnection(_ context.Context, p logging.Perspective, odcid logging.ConnectionID) logging.ConnectionTracer {
|
||||
connID := logging.ConnectionID(odcid).String()
|
||||
ql := &quicLogger{
|
||||
logger: t.logger,
|
||||
connectionID: connID,
|
||||
}
|
||||
if t.config.isClient {
|
||||
return newConnTracer(ql, p, odcid, newClientCollector(t.config.index))
|
||||
}
|
||||
return newConnTracer(ql, p, odcid, newServiceCollector())
|
||||
}
|
||||
|
||||
func (*tracer) SentPacket(net.Addr, *logging.Header, logging.ByteCount, []logging.Frame) {}
|
||||
func (*tracer) DroppedPacket(net.Addr, logging.PacketType, logging.ByteCount, logging.PacketDropReason) {
|
||||
}
|
||||
|
||||
// connTracer is a wrapper around https://pkg.go.dev/github.com/lucas-clemente/quic-go@v0.23.0/qlog#NewConnectionTracer to collect metrics
|
||||
type connTracer struct {
|
||||
logging.ConnectionTracer
|
||||
metricsCollector MetricsCollector
|
||||
connectionID string
|
||||
}
|
||||
|
||||
func newConnTracer(ql *quicLogger, p logging.Perspective, odcid logging.ConnectionID, metricsCollector MetricsCollector) logging.ConnectionTracer {
|
||||
return &connTracer{
|
||||
qlog.NewConnectionTracer(ql, p, odcid),
|
||||
metricsCollector,
|
||||
logging.ConnectionID(odcid).String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ct *connTracer) StartedConnection(local, remote net.Addr, srcConnID, destConnID logging.ConnectionID) {
|
||||
ct.metricsCollector.startedConnection()
|
||||
ct.ConnectionTracer.StartedConnection(local, remote, srcConnID, destConnID)
|
||||
}
|
||||
|
||||
func (ct *connTracer) ClosedConnection(err error) {
|
||||
ct.metricsCollector.closedConnection(err)
|
||||
ct.ConnectionTracer.ClosedConnection(err)
|
||||
}
|
||||
|
||||
func (ct *connTracer) SentPacket(hdr *logging.ExtendedHeader, packetSize logging.ByteCount, ack *logging.AckFrame, frames []logging.Frame) {
|
||||
ct.metricsCollector.sentPackets(packetSize)
|
||||
ct.ConnectionTracer.SentPacket(hdr, packetSize, ack, frames)
|
||||
}
|
||||
|
||||
func (ct *connTracer) ReceivedPacket(hdr *logging.ExtendedHeader, size logging.ByteCount, frames []logging.Frame) {
|
||||
ct.metricsCollector.receivedPackets(size)
|
||||
ct.ConnectionTracer.ReceivedPacket(hdr, size, frames)
|
||||
}
|
||||
|
||||
func (ct *connTracer) BufferedPacket(pt logging.PacketType) {
|
||||
ct.metricsCollector.bufferedPackets(pt)
|
||||
ct.ConnectionTracer.BufferedPacket(pt)
|
||||
}
|
||||
|
||||
func (ct *connTracer) DroppedPacket(pt logging.PacketType, size logging.ByteCount, reason logging.PacketDropReason) {
|
||||
ct.metricsCollector.droppedPackets(pt, size, reason)
|
||||
ct.ConnectionTracer.DroppedPacket(pt, size, reason)
|
||||
}
|
||||
|
||||
func (ct *connTracer) LostPacket(level logging.EncryptionLevel, number logging.PacketNumber, reason logging.PacketLossReason) {
|
||||
ct.metricsCollector.lostPackets(reason)
|
||||
ct.ConnectionTracer.LostPacket(level, number, reason)
|
||||
}
|
||||
|
||||
func (ct *connTracer) UpdatedMetrics(rttStats *logging.RTTStats, cwnd, bytesInFlight logging.ByteCount, packetsInFlight int) {
|
||||
ct.metricsCollector.updatedRTT(rttStats)
|
||||
ct.ConnectionTracer.UpdatedMetrics(rttStats, cwnd, bytesInFlight, packetsInFlight)
|
||||
}
|
||||
|
||||
type quicLogger struct {
|
||||
logger *zerolog.Logger
|
||||
connectionID string
|
||||
}
|
||||
|
||||
func (qt *quicLogger) Write(p []byte) (n int, err error) {
|
||||
qt.logger.Trace().Str("quicConnection", qt.connectionID).RawJSON("event", p).Msg("Quic event")
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (*quicLogger) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM python:3-buster
|
||||
|
||||
RUN wget https://bin.equinox.io/c/VdrWdbjqyF/cloudflared-stable-linux-amd64.deb \
|
||||
&& dpkg -i cloudflared-stable-linux-amd64.deb
|
||||
RUN wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb \
|
||||
&& dpkg -i cloudflared-linux-amd64.deb
|
||||
|
||||
RUN pip install pexpect
|
||||
|
||||
|
||||
+1
-1
@@ -278,7 +278,7 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
|
||||
// 403/401 from the edge will have aud in a header
|
||||
aud = audHeader
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to get Access app info for %s", reqURL.String())
|
||||
return nil, fmt.Errorf("failed to find Access application at %s", reqURL.String())
|
||||
}
|
||||
|
||||
domain := resp.Header.Get(appDomainHeader)
|
||||
|
||||
+3
-23
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
baseStoreURL = "https://login.argotunnel.com/"
|
||||
baseStoreURL = "https://login.cloudflareaccess.org/"
|
||||
clientTimeout = time.Second * 60
|
||||
)
|
||||
|
||||
@@ -83,11 +83,13 @@ func buildRequestURL(baseURL *url.URL, key, value string, cli, useHostOnly bool)
|
||||
if useHostOnly {
|
||||
baseURL.Path = ""
|
||||
}
|
||||
// TODO: pass arg for tunnel login
|
||||
if !cli {
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
|
||||
q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token
|
||||
q.Set("edge_token_transfer", "true") // use new LoginHelper service built on workers
|
||||
baseURL.RawQuery = q.Encode() // and this actual baseURL.
|
||||
baseURL.Path = "cdn-cgi/access/cli"
|
||||
return baseURL.String(), nil
|
||||
@@ -103,9 +105,6 @@ func transferRequest(requestURL string, log *zerolog.Logger) ([]byte, string, er
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
} else if len(buf) > 0 {
|
||||
if err := putSuccess(client, requestURL); err != nil {
|
||||
log.Err(err).Msg("Failed to update resource success")
|
||||
}
|
||||
return buf, key, nil
|
||||
}
|
||||
}
|
||||
@@ -136,22 +135,3 @@ func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte,
|
||||
}
|
||||
return buf.Bytes(), resp.Header.Get("service-public-key"), nil
|
||||
}
|
||||
|
||||
// putSuccess tells the server we successfully downloaded the resource
|
||||
func putSuccess(client *http.Client, requestURL string) error {
|
||||
req, err := http.NewRequest("PUT", requestURL+"/ok", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("HTTP Response Status Code: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+22
-8
@@ -81,27 +81,32 @@ type RouteResult interface {
|
||||
}
|
||||
|
||||
type DNSRoute struct {
|
||||
userHostname string
|
||||
userHostname string
|
||||
overwriteExisting bool
|
||||
}
|
||||
|
||||
type DNSRouteResult struct {
|
||||
route *DNSRoute
|
||||
CName Change `json:"cname"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewDNSRoute(userHostname string) Route {
|
||||
func NewDNSRoute(userHostname string, overwriteExisting bool) Route {
|
||||
return &DNSRoute{
|
||||
userHostname: userHostname,
|
||||
userHostname: userHostname,
|
||||
overwriteExisting: overwriteExisting,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
OverwriteExisting bool `json:"overwrite_existing"`
|
||||
}{
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
OverwriteExisting: dr.overwriteExisting,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
@@ -131,7 +136,16 @@ func (res *DNSRouteResult) SuccessSummary() string {
|
||||
case ChangeUnchanged:
|
||||
msgFmt = "%s is already configured to route to your tunnel"
|
||||
}
|
||||
return fmt.Sprintf(msgFmt, res.route.userHostname)
|
||||
return fmt.Sprintf(msgFmt, res.hostname())
|
||||
}
|
||||
|
||||
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
||||
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
||||
func (res *DNSRouteResult) hostname() string {
|
||||
if res.Name != "" {
|
||||
return res.Name
|
||||
}
|
||||
return res.route.userHostname
|
||||
}
|
||||
|
||||
type LBRoute struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package tunnelstore
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -25,6 +26,14 @@ func (f *Filter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *Filter) ByNamePrefix(namePrefix string) {
|
||||
f.queryParams.Set("name_prefix", namePrefix)
|
||||
}
|
||||
|
||||
func (f *Filter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||
f.queryParams.Set("exclude_prefix", excludePrefix)
|
||||
}
|
||||
|
||||
func (f *Filter) NoDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
@@ -37,6 +46,10 @@ func (f *Filter) ByTunnelID(tunnelID uuid.UUID) {
|
||||
f.queryParams.Set("uuid", tunnelID.String())
|
||||
}
|
||||
|
||||
func (f *Filter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f Filter) encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func validateScheme(scheme string) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Currently Argo Tunnel does not support %s protocol.", scheme)
|
||||
return fmt.Errorf("Currently Cloudflare Tunnel does not support %s protocol.", scheme)
|
||||
}
|
||||
|
||||
func validateIP(scheme, host, port string) (string, error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user