mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 23:31:56 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0269062b24 | |||
| bccf650165 | |||
| cecc74d3fb | |||
| 18992efa0c | |||
| bd8af7d80d | |||
| dff694b218 | |||
| 38af26e232 | |||
| 8f3526289a | |||
| 81dff44bb9 | |||
| 6e45e0d53b | |||
| a7d2de1e12 | |||
| 59cae0f622 | |||
| d678584d89 | |||
| f1b57526b3 | |||
| 3eb9efd9f0 | |||
| 8d99e92852 | |||
| 31ff7caeeb | |||
| 5d84874d53 | |||
| b06fe0fc5f | |||
| bf068e728f | |||
| f88732277a | |||
| 98a0844f56 | |||
| d6d65d183b | |||
| 951d13d76c | |||
| f99ae90ca1 | |||
| e99d0b5bc4 | |||
| 48c5721bc6 | |||
| 235897ba21 | |||
| bf737a0efc | |||
| 4c5ebccacc | |||
| a3153c6add | |||
| a2bbe1bdc2 | |||
| 6526211a69 | |||
| 103b2eca00 | |||
| 14ea6298bc | |||
| b297e8bb90 | |||
| 6cc8c39fa8 | |||
| 4d4eedd1c0 | |||
| a00eda9538 | |||
| 9ff65c1e90 | |||
| 2dcf3bd011 | |||
| ae460b340b | |||
| bc54a7f87b | |||
| 75c3ca2f4a | |||
| 4bd17766a9 | |||
| b87cb9aee8 | |||
| 07af2a33b7 | |||
| 209091da39 | |||
| 4d43a70a38 | |||
| 5b35e968f3 | |||
| f3b0b33dc5 | |||
| aca0c93461 | |||
| eed7d7bbc9 | |||
| 1073f8db40 |
@@ -0,0 +1,20 @@
|
||||
on: [push, pull_request]
|
||||
name: Check
|
||||
jobs:
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.15.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Install go-sumtype
|
||||
run: go get github.com/BurntSushi/go-sumtype
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Test
|
||||
run: make test
|
||||
@@ -15,3 +15,4 @@ cscope.*
|
||||
*-session.log
|
||||
ssh_server_tests/.env
|
||||
/.cover
|
||||
built_artifacts/
|
||||
|
||||
+43
-3
@@ -1,5 +1,46 @@
|
||||
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
|
||||
|
||||
## 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
|
||||
is compatible with any client version that uses Named Tunnels.
|
||||
|
||||
To get started, visit our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/deploy-cloudflared-replicas).
|
||||
- `cloudflared tunnel ingress validate` will now warn about unused keys in your config file. This is helpful for
|
||||
detecting typos in your config.
|
||||
- If `cloudflared` detects it is running inside a Linux container, it will limit itself to use only the number of CPUs
|
||||
the pod has been granted, instead of trying to use every CPU available.
|
||||
|
||||
## 2021.4.0
|
||||
|
||||
### Bug Fixes
|
||||
@@ -31,7 +72,7 @@
|
||||
### Improvements
|
||||
|
||||
- Tunnel create command, as well as, running ad-hoc tunnels using `cloudflared tunnel -name NAME`, will not overwrite
|
||||
existing files when writing tunnel credentials.
|
||||
existing files when writing tunnel credentials.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -77,7 +118,7 @@ ingress:
|
||||
ports: [80, 443]
|
||||
allow: true
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
@@ -129,4 +170,3 @@ ingress:
|
||||
|
||||
- The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared
|
||||
exhausting CPU usage when faced with connectivity issues.
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# use a builder image for building cloudflare
|
||||
ARG TARGET_GOOS
|
||||
ARG TARGET_GOARCH
|
||||
FROM golang:1.15.7 as builder
|
||||
FROM golang:1.16.4 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)"'
|
||||
|
||||
@@ -81,7 +88,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
|
||||
@@ -138,7 +145,7 @@ define build_package
|
||||
--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 +157,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
|
||||
@@ -224,6 +239,10 @@ release: bin/equinox
|
||||
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)
|
||||
@@ -237,24 +256,25 @@ 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-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,3 +1,72 @@
|
||||
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
|
||||
|
||||
|
||||
2021.5.4
|
||||
- Fix release pipeline
|
||||
|
||||
2021.5.1
|
||||
- 2021-05-10 TUN-4342: Fix false positive warning about unused hostname property
|
||||
- 2021-05-10 Release 2021.5.0
|
||||
|
||||
2021.5.0
|
||||
- 2021-05-10 TUN-4384: Silence log from automaxprocs
|
||||
- 2021-05-10 AUTH-3537: AUDs in JWTs are now always arrays
|
||||
- 2021-05-10 Update changelog for 2021.5.0
|
||||
- 2021-05-03 TUN-4343: Fix broken build by setting debug field correctly
|
||||
- 2021-05-06 TUN-4356: Set AUTOMAXPROCS to the CPU limit when running in a Linux container
|
||||
- 2021-05-06 TUN-4357: Bump Go to 1.16
|
||||
- 2021-05-06 TUN-4359: Warn about unused keys in 'tunnel ingress validate'
|
||||
- 2021-04-30 debug: log host / path
|
||||
- 2021-04-20 AUTH-3513: Checks header for app info in case response is a 403/401 from the edge
|
||||
- 2021-04-29 TUN-4000: Release notes for cloudflared replica model
|
||||
- 2021-04-09 TUN-2853: rename STDIN-CONTROL env var to STDIN_CONTROL
|
||||
- 2021-04-09 TUN-4206: Better error message when user is only using one ingress rule
|
||||
|
||||
2021.4.0
|
||||
- 2021-04-05 TUN-4178: Fix component test for running as a service in MacOS to not assume a named tunnel
|
||||
- 2021-04-05 TUN-4177: Running with proxy-dns should not prevent running Named Tunnels
|
||||
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
# 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
|
||||
done
|
||||
|
||||
mv *.msi $ARTIFACT_DIR
|
||||
|
||||
linuxArchs=("amd64" "386" "arm")
|
||||
export TARGET_OS=linux
|
||||
for arch in ${linuxArchs[@]}; do
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-deb
|
||||
make cloudflared-rpm
|
||||
done
|
||||
|
||||
mv *.deb $ARTIFACT_DIR
|
||||
mv *.rpm $ARTIFACT_DIR
|
||||
+60
-11
@@ -1,4 +1,4 @@
|
||||
pinned_go: &pinned_go go=1.15.7-1
|
||||
pinned_go: &pinned_go go=1.16.3-1
|
||||
pinned_go_fips: &pinned_go_fips go-fips=1.15.5-3
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
@@ -14,25 +14,72 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
build-deb:
|
||||
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:
|
||||
- ./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-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
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- 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
|
||||
@@ -279,8 +326,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.15.7.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.7.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 +339,9 @@ centos-7:
|
||||
builddeps: *el7_builddeps
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.15.7.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="all"
|
||||
Action="set"
|
||||
System="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
|
||||
|
||||
<Feature Id='Complete' Level='1'>
|
||||
<ComponentRef Id="ENVS"/>
|
||||
<ComponentRef Id='ApplicationFiles' />
|
||||
</Feature>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -13,27 +13,38 @@ func Action(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
}
|
||||
|
||||
func ConfiguredAction(actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||
// Adapt actionFunc to the type signature required by ConfiguredActionWithWarnings
|
||||
f := func(context *cli.Context, _ string) error {
|
||||
return actionFunc(context)
|
||||
}
|
||||
|
||||
return ConfiguredActionWithWarnings(f)
|
||||
}
|
||||
|
||||
// Just like ConfiguredAction, but accepts a second parameter with configuration warnings.
|
||||
func ConfiguredActionWithWarnings(actionFunc func(*cli.Context, string) error) cli.ActionFunc {
|
||||
return WithErrorHandler(func(c *cli.Context) error {
|
||||
if err := setFlagsFromConfigFile(c); err != nil {
|
||||
warnings, err := setFlagsFromConfigFile(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return actionFunc(c)
|
||||
return actionFunc(c, warnings)
|
||||
})
|
||||
}
|
||||
|
||||
func setFlagsFromConfigFile(c *cli.Context) error {
|
||||
func setFlagsFromConfigFile(c *cli.Context) (configWarnings string, err error) {
|
||||
const errorExitCode = 1
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
inputSource, err := config.ReadConfigFile(c, log)
|
||||
inputSource, warnings, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
if err == config.ErrNoConfigFile {
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
return cli.Exit(err, errorExitCode)
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
|
||||
if err := altsrc.ApplyInputSource(c, inputSource); err != nil {
|
||||
return cli.Exit(err, errorExitCode)
|
||||
return "", cli.Exit(err, errorExitCode)
|
||||
}
|
||||
return nil
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ func installLinuxService(c *cli.Context) error {
|
||||
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
|
||||
}
|
||||
} else {
|
||||
src, err := config.ReadConfigFile(c, log)
|
||||
src, _, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
@@ -47,6 +48,7 @@ func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
maxprocs.Set()
|
||||
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 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).Msg("failed to create route, please create it manually")
|
||||
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 {
|
||||
@@ -595,7 +614,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: "stdin-control",
|
||||
Usage: "Control the process using commands sent through stdin",
|
||||
EnvVars: []string{"STDIN-CONTROL"},
|
||||
EnvVars: []string{"STDIN_CONTROL"},
|
||||
Hidden: true,
|
||||
Value: false,
|
||||
}),
|
||||
@@ -612,7 +631,14 @@ 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,
|
||||
}),
|
||||
selectProtocolFlag,
|
||||
overwriteDNSFlag,
|
||||
}...)
|
||||
|
||||
return flags
|
||||
@@ -699,7 +725,7 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: ingress.ProxyTCPKeepAlive,
|
||||
Name: ingress.ProxyTCPKeepAliveFlag,
|
||||
Usage: "HTTP proxy TCP keepalive duration",
|
||||
Value: time.Second * 30,
|
||||
Hidden: shouldHide,
|
||||
|
||||
@@ -160,12 +160,8 @@ func prepareTunnelConfig(
|
||||
if err != nil {
|
||||
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
|
||||
return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname")
|
||||
} else if hostname != "" && isNamedTunnel {
|
||||
log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " +
|
||||
"in the property `tunnel`. Make sure to provision the routing (e.g. via `cloudflared tunnel route`) or else " +
|
||||
"your origin will not be reachable. You should remove the `hostname` property to avoid this warning.")
|
||||
}
|
||||
isFreeTunnel := hostname == ""
|
||||
isQuickTunnel := hostname == ""
|
||||
clientID := c.String("id")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(log)
|
||||
@@ -183,7 +179,7 @@ func prepareTunnelConfig(
|
||||
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||||
|
||||
var originCert []byte
|
||||
if !isFreeTunnel {
|
||||
if !isQuickTunnel {
|
||||
originCertPath := c.String("origincert")
|
||||
originCertLog := log.With().
|
||||
Str(LogFieldOriginCertPath, originCertPath).
|
||||
@@ -282,7 +278,6 @@ func prepareTunnelConfig(
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
IsFreeTunnel: isFreeTunnel,
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
|
||||
@@ -45,7 +45,7 @@ func buildIngressSubcommand() *cli.Command {
|
||||
func buildValidateIngressCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "validate",
|
||||
Action: cliutil.ConfiguredAction(validateIngressCommand),
|
||||
Action: cliutil.ConfiguredActionWithWarnings(validateIngressCommand),
|
||||
Usage: "Validate the ingress configuration ",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
|
||||
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
|
||||
@@ -68,7 +68,7 @@ func buildTestURLCommand() *cli.Command {
|
||||
}
|
||||
|
||||
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
|
||||
func validateIngressCommand(c *cli.Context) error {
|
||||
func validateIngressCommand(c *cli.Context, warnings string) error {
|
||||
conf := config.GetConfiguration()
|
||||
if conf.Source() == "" {
|
||||
fmt.Println("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
|
||||
@@ -81,6 +81,11 @@ func validateIngressCommand(c *cli.Context) error {
|
||||
if c.IsSet("url") {
|
||||
return ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
if warnings != "" {
|
||||
fmt.Println("Warning: unused keys detected in your config file. Here is a list of unused keys:")
|
||||
fmt.Println(warnings)
|
||||
return nil
|
||||
}
|
||||
fmt.Println("OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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,111 @@
|
||||
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
|
||||
|
||||
// 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("Requesting new Quick Tunnel...")
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
for _, line := range AsciiBox([]string{
|
||||
"Your Quick Tunnel has been created! Visit it at:",
|
||||
data.Result.Hostname,
|
||||
}, 2) {
|
||||
sc.log.Info().Msg(line)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
|
||||
@@ -29,10 +29,11 @@ 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"
|
||||
overwriteDNSFlagName = "overwrite-dns"
|
||||
|
||||
LogFieldTunnelID = "tunnelID"
|
||||
)
|
||||
@@ -110,7 +111,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 +134,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 {
|
||||
@@ -595,6 +602,12 @@ func runCommand(c *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.String("hostname") != "" {
|
||||
sc.log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " +
|
||||
"in the property `tunnel` to run. Make sure to provision the routing (e.g. via `cloudflared tunnel route dns/lb`) or else " +
|
||||
"your origin will not be reachable. You should remove the `hostname` property to avoid this warning.")
|
||||
}
|
||||
|
||||
return runNamedTunnel(sc, tunnelRef)
|
||||
}
|
||||
|
||||
@@ -642,7 +655,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.
|
||||
@@ -662,15 +674,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())
|
||||
@@ -681,14 +708,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())
|
||||
@@ -730,41 +757,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)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -95,7 +94,7 @@ func TestTunnelfilePath(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
homeDir, err := homedir.Dir()
|
||||
assert.NoError(t, err)
|
||||
expected := fmt.Sprintf("%s/.cloudflared/%v.json", homeDir, tunnelID)
|
||||
expected := filepath.Join(homeDir, ".cloudflared", tunnelID.String()+".json")
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
@@ -13,6 +15,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -21,6 +24,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testFilePath = filepath.Join(os.TempDir(), "test")
|
||||
|
||||
func respondWithJSON(w http.ResponseWriter, v interface{}, status int) {
|
||||
data, _ := json.Marshal(v)
|
||||
|
||||
@@ -208,7 +213,6 @@ func TestUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
log.Println("server url: ", ts.URL)
|
||||
@@ -229,7 +233,6 @@ func TestBetaUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -249,7 +252,6 @@ func TestFailUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -263,7 +265,6 @@ func TestNoUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -278,7 +279,6 @@ func TestForcedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -298,7 +298,6 @@ func TestUpdateSpecificVersionService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
reqVersion := "2020.9.1"
|
||||
@@ -319,7 +318,6 @@ func TestCompressedUpdateService(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
@@ -339,7 +337,6 @@ func TestUpdateWhenRunningKnownBuggyVersion(t *testing.T) {
|
||||
ts := createServer()
|
||||
defer ts.Close()
|
||||
|
||||
testFilePath := "tmpfile"
|
||||
createTestFile(t, testFilePath)
|
||||
defer os.Remove(testFilePath)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
#!/usr/bin/env python
|
||||
import copy
|
||||
import platform
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
from flaky import flaky
|
||||
|
||||
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:
|
||||
# Repeat the test multiple times because some issues only occur after multiple reconnects
|
||||
self.assert_reconnect(config, cloudflared, 5)
|
||||
|
||||
@pytest.mark.skipif(platform.system() == "Windows", reason=f"Currently buggy on Windows TUN-4699")
|
||||
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)
|
||||
with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=True) as cloudflared:
|
||||
self.assert_reconnect(config, cloudflared, 1)
|
||||
|
||||
def send_reconnect(self, cloudflared, secs):
|
||||
@@ -36,7 +40,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 +48,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+18
-7
@@ -358,13 +358,13 @@ func GetConfiguration() *Configuration {
|
||||
// ReadConfigFile returns InputSourceContext initialized from the configuration file.
|
||||
// On repeat calls returns with the same file, returns without reading the file again; however,
|
||||
// if value of "config" flag changes, will read the new config file
|
||||
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, error) {
|
||||
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSettings, warnings string, err error) {
|
||||
configFile := c.String("config")
|
||||
if configuration.Source() == configFile || configFile == "" {
|
||||
if configuration.Source() == "" {
|
||||
return nil, ErrNoConfigFile
|
||||
return nil, "", ErrNoConfigFile
|
||||
}
|
||||
return &configuration, nil
|
||||
return &configuration, "", nil
|
||||
}
|
||||
|
||||
log.Debug().Msgf("Loading configuration from %s", configFile)
|
||||
@@ -373,16 +373,27 @@ func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, e
|
||||
if os.IsNotExist(err) {
|
||||
err = ErrNoConfigFile
|
||||
}
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
|
||||
if err == io.EOF {
|
||||
log.Error().Msgf("Configuration file %s was empty", configFile)
|
||||
return &configuration, nil
|
||||
return &configuration, "", nil
|
||||
}
|
||||
return nil, errors.Wrap(err, "error parsing YAML in config file at "+configFile)
|
||||
return nil, "", errors.Wrap(err, "error parsing YAML in config file at "+configFile)
|
||||
}
|
||||
configuration.sourceFile = configFile
|
||||
return &configuration, nil
|
||||
|
||||
// Parse it again, with strict mode, to find warnings.
|
||||
if file, err := os.Open(configFile); err == nil {
|
||||
decoder := yaml.NewDecoder(file)
|
||||
decoder.SetStrict(true)
|
||||
var unusedConfig configFileSettings
|
||||
if err := decoder.Decode(&unusedConfig); err != nil {
|
||||
warnings = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return &configuration, warnings, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,9 +12,15 @@ 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"
|
||||
)
|
||||
|
||||
var switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
|
||||
type Config struct {
|
||||
OriginProxy OriginProxy
|
||||
@@ -22,8 +29,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 +56,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 +91,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 +171,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,6 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,7 +21,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
testConfig = &Config{
|
||||
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
|
||||
testConfig = &Config{
|
||||
OriginProxy: &mockOriginProxy{},
|
||||
GracePeriod: time.Millisecond * 100,
|
||||
}
|
||||
@@ -38,14 +42,17 @@ 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 {
|
||||
return wsEndpoint(w, req)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
switch req.URL.Path {
|
||||
case "/ok":
|
||||
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
||||
case "/large_file":
|
||||
@@ -60,6 +67,15 @@ func (moc *mockOriginProxy) Proxy(w ResponseWriter, r *http.Request, sourceConne
|
||||
originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (moc *mockOriginProxy) ProxyTCP(
|
||||
ctx context.Context,
|
||||
rwa ReadWriteAcker,
|
||||
r *TCPRequest,
|
||||
) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type nowriter struct {
|
||||
|
||||
+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) {
|
||||
|
||||
+74
-15
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
@@ -26,7 +27,9 @@ const (
|
||||
|
||||
var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
|
||||
|
||||
type http2Connection struct {
|
||||
// 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
|
||||
@@ -38,6 +41,7 @@ type http2Connection struct {
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
log *zerolog.Logger
|
||||
activeRequestsWG sync.WaitGroup
|
||||
connectedFuse ConnectedFuse
|
||||
gracefulShutdownC <-chan struct{}
|
||||
@@ -45,6 +49,7 @@ type http2Connection struct {
|
||||
controlStreamErr error // result of running control stream handler
|
||||
}
|
||||
|
||||
// NewHTTP2Connection returns a new instance of HTTP2Connection.
|
||||
func NewHTTP2Connection(
|
||||
conn net.Conn,
|
||||
config *Config,
|
||||
@@ -53,9 +58,10 @@ func NewHTTP2Connection(
|
||||
observer *Observer,
|
||||
connIndex uint8,
|
||||
connectedFuse ConnectedFuse,
|
||||
log *zerolog.Logger,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) *http2Connection {
|
||||
return &http2Connection{
|
||||
) *HTTP2Connection {
|
||||
return &HTTP2Connection{
|
||||
conn: conn,
|
||||
server: &http2.Server{
|
||||
MaxConcurrentStreams: math.MaxUint32,
|
||||
@@ -68,11 +74,13 @@ func NewHTTP2Connection(
|
||||
connIndex: connIndex,
|
||||
newRPCClientFunc: newRegistrationRPCClient,
|
||||
connectedFuse: connectedFuse,
|
||||
log: log,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -93,34 +101,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)
|
||||
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.serveControlStream(r.Context(), respWriter); 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 {
|
||||
func (c *HTTP2Connection) serveControlStream(ctx context.Context, respWriter *http2RespWriter) error {
|
||||
rpcClient := c.newRPCClientFunc(ctx, respWriter, c.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
@@ -143,7 +177,7 @@ func (c *http2Connection) serveControlStream(ctx context.Context, respWriter *ht
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *http2Connection) close() {
|
||||
func (c *HTTP2Connection) close() {
|
||||
// Wait for all serve HTTP handlers to return
|
||||
c.activeRequestsWG.Wait()
|
||||
c.conn.Close()
|
||||
@@ -255,6 +289,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 +319,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")
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@ var (
|
||||
testTransport = http2.Transport{}
|
||||
)
|
||||
|
||||
func newTestHTTP2Connection() (*http2Connection, net.Conn) {
|
||||
func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
|
||||
edgeConn, originConn := net.Pipe()
|
||||
var connIndex = uint8(0)
|
||||
log := zerolog.Nop()
|
||||
return NewHTTP2Connection(
|
||||
originConn,
|
||||
testConfig,
|
||||
@@ -37,6 +38,7 @@ func newTestHTTP2Connection() (*http2Connection, net.Conn) {
|
||||
NewObserver(&log, &log, false),
|
||||
connIndex,
|
||||
mockConnectedFuse{},
|
||||
&log,
|
||||
nil,
|
||||
), edgeConn
|
||||
}
|
||||
|
||||
+1
-54
@@ -1,13 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,53 +48,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,7 +56,7 @@ 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})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.15.7 as builder
|
||||
FROM golang:1.16.4 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
+66
-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,57 @@ 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 compile_regex() :
|
||||
# The capture groups are:
|
||||
# 1: the text cloudflared
|
||||
# 2: compiled architecture
|
||||
# 3: possible extensions
|
||||
capture_string = r'(cloudflared).+(amd64|x86_64|386|arm)(\.msi|\.deb|\.rpm|\.exe)?'
|
||||
return re.compile(capture_string)
|
||||
|
||||
def extract_filename(regex, filename):
|
||||
match_groups = regex.match(filename)
|
||||
os = "linux"
|
||||
name = match_groups.group(1)
|
||||
arch = match_groups.group(2)
|
||||
ext = match_groups.group(3)
|
||||
if ext in ['.msi', '.exe']:
|
||||
os = "windows"
|
||||
return "{}-{}-{}{}".format(name, os, arch, ext if ext is not None else "")
|
||||
|
||||
|
||||
def main():
|
||||
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
|
||||
@@ -174,40 +228,20 @@ 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):
|
||||
regexp = compile_regex()
|
||||
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)
|
||||
filename = extract_filename(regexp, 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()
|
||||
|
||||
@@ -43,6 +43,7 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
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/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
|
||||
|
||||
@@ -609,6 +609,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0=
|
||||
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
|
||||
+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())
|
||||
}
|
||||
}
|
||||
|
||||
+7
-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
|
||||
}
|
||||
@@ -138,6 +138,11 @@ func (ing Ingress) IsEmpty() bool {
|
||||
return len(ing.Rules) == 0
|
||||
}
|
||||
|
||||
// IsSingleRule checks if the user only specified a single ingress rule.
|
||||
func (ing Ingress) IsSingleRule() bool {
|
||||
return len(ing.Rules) == 1
|
||||
}
|
||||
|
||||
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
|
||||
func (ing Ingress) StartOrigins(
|
||||
wg *sync.WaitGroup,
|
||||
@@ -162,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
|
||||
|
||||
@@ -402,7 +402,7 @@ func TestSingleOriginSetsConfig(t *testing.T) {
|
||||
flagSet.Bool("hello-world", true, "")
|
||||
flagSet.Duration(ProxyConnectTimeoutFlag, time.Second, "")
|
||||
flagSet.Duration(ProxyTLSTimeoutFlag, time.Second, "")
|
||||
flagSet.Duration(ProxyTCPKeepAlive, time.Second, "")
|
||||
flagSet.Duration(ProxyTCPKeepAliveFlag, time.Second, "")
|
||||
flagSet.Bool(ProxyNoHappyEyeballsFlag, true, "")
|
||||
flagSet.Int(ProxyKeepAliveConnectionsFlag, 10, "")
|
||||
flagSet.Duration(ProxyKeepAliveTimeoutFlag, time.Second, "")
|
||||
@@ -423,7 +423,7 @@ func TestSingleOriginSetsConfig(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
err = cliCtx.Set(ProxyTLSTimeoutFlag, "1s")
|
||||
require.NoError(t, err)
|
||||
err = cliCtx.Set(ProxyTCPKeepAlive, "1s")
|
||||
err = cliCtx.Set(ProxyTCPKeepAliveFlag, "1s")
|
||||
require.NoError(t, err)
|
||||
err = cliCtx.Set(ProxyNoHappyEyeballsFlag, "true")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -190,71 +189,6 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamWSConnection(t *testing.T) {
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
|
||||
origin := echoWSOrigin(t, true)
|
||||
defer origin.Close()
|
||||
|
||||
var svc httpService
|
||||
err := svc.start(&sync.WaitGroup{}, testLogger, nil, nil, OriginRequestConfig{
|
||||
NoTLSVerify: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
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)
|
||||
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()
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
echoWSEyeball(t, eyeballConn)
|
||||
fmt.Println("closing pipe")
|
||||
edgeConn.Close()
|
||||
return eyeballConn.Close()
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
defer conn.Close()
|
||||
conn.Stream(ctx, edgeConn, testLogger)
|
||||
close(connClosed)
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
type wsEyeball struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
+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{}) {
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
Socks5Flag = "socks5"
|
||||
ProxyConnectTimeoutFlag = "proxy-connect-timeout"
|
||||
ProxyTLSTimeoutFlag = "proxy-tls-timeout"
|
||||
ProxyTCPKeepAlive = "proxy-tcp-keepalive"
|
||||
ProxyTCPKeepAliveFlag = "proxy-tcp-keepalive"
|
||||
ProxyNoHappyEyeballsFlag = "proxy-no-happy-eyeballs"
|
||||
ProxyKeepAliveConnectionsFlag = "proxy-keepalive-connections"
|
||||
ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout"
|
||||
@@ -60,7 +60,7 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
|
||||
if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) {
|
||||
tlsTimeout = c.Duration(flag)
|
||||
}
|
||||
if flag := ProxyTCPKeepAlive; c.IsSet(flag) {
|
||||
if flag := ProxyTCPKeepAliveFlag; c.IsSet(flag) {
|
||||
tcpKeepAlive = c.Duration(flag)
|
||||
}
|
||||
if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+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)
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Timer assumes the metrics is partitioned by one label
|
||||
type Timer struct {
|
||||
startTime map[string]time.Time
|
||||
metrics *prometheus.HistogramVec
|
||||
measureUnit time.Duration
|
||||
labelKey string
|
||||
}
|
||||
|
||||
func NewTimer(metrics *prometheus.HistogramVec, unit time.Duration, labelKey string) *Timer {
|
||||
return &Timer{
|
||||
startTime: make(map[string]time.Time),
|
||||
measureUnit: unit,
|
||||
metrics: metrics,
|
||||
labelKey: labelKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Timer) Start(labelVal string) {
|
||||
i.startTime[labelVal] = time.Now()
|
||||
}
|
||||
|
||||
func (i *Timer) End(labelVal string) time.Duration {
|
||||
if start, ok := i.startTime[labelVal]; ok {
|
||||
return Latency(start, time.Now())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (i *Timer) Observe(measurement time.Duration, labelVal string) {
|
||||
metricsLabels := prometheus.Labels{i.labelKey: labelVal}
|
||||
i.metrics.With(metricsLabels).Observe(float64(measurement / i.measureUnit))
|
||||
}
|
||||
|
||||
func (i *Timer) EndAndObserve(labelVal string) {
|
||||
i.Observe(i.End(labelVal), labelVal)
|
||||
}
|
||||
|
||||
func Latency(startTime, endTime time.Time) time.Duration {
|
||||
return endTime.Sub(startTime)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEnd(t *testing.T) {
|
||||
m := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: "TestCallLatencyWithoutMeasurement",
|
||||
Name: "Latency",
|
||||
Buckets: prometheus.LinearBuckets(0, 50, 100),
|
||||
},
|
||||
[]string{"key"},
|
||||
)
|
||||
timer := NewTimer(m, time.Millisecond, "key")
|
||||
assert.Equal(t, time.Duration(0), timer.End("dne"))
|
||||
timer.Start("test")
|
||||
assert.NotEqual(t, time.Duration(0), timer.End("test"))
|
||||
}
|
||||
+173
-97
@@ -7,21 +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-"
|
||||
// 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
|
||||
@@ -29,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,
|
||||
@@ -44,35 +51,18 @@ 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()
|
||||
|
||||
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
|
||||
}
|
||||
cfRay := connection.FindCfRayHeader(req)
|
||||
lbProbe := connection.IsLBProbeRequest(req)
|
||||
|
||||
rule, ruleNum := p.ingressRules.FindMatchingRule(req.Host, req.URL.Path)
|
||||
logFields := logFields{
|
||||
@@ -82,49 +72,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, 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, 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 (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)
|
||||
func ruleField(ing ingress.Ingress, ruleNum int) (ruleID string, srv string) {
|
||||
srv = ing.Rules[ruleNum].Service.String()
|
||||
if ing.IsSingleRule() {
|
||||
return "", srv
|
||||
}
|
||||
return fmt.Sprintf("%d", ruleNum), srv
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -132,6 +186,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)
|
||||
@@ -142,32 +213,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() {
|
||||
@@ -176,12 +245,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
|
||||
}
|
||||
|
||||
@@ -198,7 +262,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')
|
||||
@@ -209,7 +273,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)
|
||||
}
|
||||
@@ -221,7 +285,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 {
|
||||
@@ -229,8 +293,13 @@ func (p *proxy) logRequest(r *http.Request, fields logFields) {
|
||||
} else {
|
||||
p.log.Debug().Msgf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", r.Method, r.URL, r.Proto)
|
||||
}
|
||||
p.log.Debug().Msgf("CF-RAY: %s Request Headers %+v", fields.cfRay, r.Header)
|
||||
p.log.Debug().Msgf("CF-RAY: %s Serving with ingress rule %v", fields.cfRay, fields.rule)
|
||||
p.log.Debug().
|
||||
Str("CF-RAY", fields.cfRay).
|
||||
Str("Header", fmt.Sprintf("%+v", r.Header)).
|
||||
Str("host", r.Host).
|
||||
Str("path", r.URL.Path).
|
||||
Interface("rule", fields.rule).
|
||||
Msg("Inbound request")
|
||||
|
||||
if contentLen := r.ContentLength; contentLen == -1 {
|
||||
p.log.Debug().Msgf("CF-RAY: %s Request Content length unknown", fields.cfRay)
|
||||
@@ -239,7 +308,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)
|
||||
@@ -257,19 +326,26 @@ func (p *proxy) logOriginResponse(resp *http.Response, fields logFields) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxy) logRequestError(err error, cfRay string, rule interface{}) {
|
||||
func (p *Proxy) logRequestError(err error, cfRay string, rule, service string) {
|
||||
requestErrors.Inc()
|
||||
log := p.log.Error().Err(err)
|
||||
if cfRay != "" {
|
||||
p.log.Error().Msgf("CF-RAY: %s Proxying to ingress %v error: %v", cfRay, rule, err)
|
||||
} else {
|
||||
p.log.Error().Msgf("Proxying to ingress %v error: %v", rule, err)
|
||||
log = log.Str(LogFieldCFRay, cfRay)
|
||||
}
|
||||
if rule != "" {
|
||||
log = log.Str(LogFieldRule, rule)
|
||||
}
|
||||
if service != "" {
|
||||
log = log.Str(LogFieldOriginService, service)
|
||||
}
|
||||
log.Msg("")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func findCfRayHeader(req *http.Request) string {
|
||||
return req.Header.Get("Cf-Ray")
|
||||
}
|
||||
|
||||
func isLBProbeRequest(req *http.Request) bool {
|
||||
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
|
||||
}
|
||||
|
||||
+99
-31
@@ -6,9 +6,11 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -44,6 +46,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,7 +150,7 @@ 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)
|
||||
|
||||
assert.Equal(t, http.StatusOK, responseWriter.Code)
|
||||
@@ -168,7 +174,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 +235,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 +279,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 +304,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 +362,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 +376,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 +575,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 +621,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 +639,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)
|
||||
@@ -633,6 +656,45 @@ func TestConnections(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type requestBody struct {
|
||||
pw *io.PipeWriter
|
||||
pr *io.PipeReader
|
||||
@@ -759,6 +821,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 +838,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)
|
||||
}
|
||||
|
||||
+1
-2
@@ -26,7 +26,6 @@ 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"
|
||||
)
|
||||
@@ -47,7 +46,6 @@ type TunnelConfig struct {
|
||||
HAConnections int
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
IsFreeTunnel bool
|
||||
LBPool string
|
||||
Tags []tunnelpogs.Tag
|
||||
Log *zerolog.Logger
|
||||
@@ -417,6 +415,7 @@ func ServeHTTP2(
|
||||
config.Observer,
|
||||
connIndex,
|
||||
connectedFuse,
|
||||
config.Log,
|
||||
gracefulShutdownC,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
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,115 @@
|
||||
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}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return writeSignature(stream)
|
||||
// TODO : TUN-4613 Write protocol version here
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package sshgen
|
||||
|
||||
import (
|
||||
|
||||
+17
-17
@@ -25,6 +25,7 @@ const (
|
||||
keyName = "token"
|
||||
tokenCookie = "CF_Authorization"
|
||||
appDomainHeader = "CF-Access-Domain"
|
||||
appAUDHeader = "CF-Access-Aud"
|
||||
AccessLoginWorkerPath = "/cdn-cgi/access/login"
|
||||
)
|
||||
|
||||
@@ -45,7 +46,7 @@ type signalHandler struct {
|
||||
signals []os.Signal
|
||||
}
|
||||
|
||||
type appJWTPayload struct {
|
||||
type jwtPayload struct {
|
||||
Aud []string `json:"aud"`
|
||||
Email string `json:"email"`
|
||||
Exp int `json:"exp"`
|
||||
@@ -56,17 +57,12 @@ type appJWTPayload struct {
|
||||
Subt string `json:"sub"`
|
||||
}
|
||||
|
||||
type orgJWTPayload struct {
|
||||
appJWTPayload
|
||||
Aud string `json:"aud"`
|
||||
}
|
||||
|
||||
type transferServiceResponse struct {
|
||||
AppToken string `json:"app_token"`
|
||||
OrgToken string `json:"org_token"`
|
||||
}
|
||||
|
||||
func (p appJWTPayload) isExpired() bool {
|
||||
func (p jwtPayload) isExpired() bool {
|
||||
return int(time.Now().Unix()) > p.Exp
|
||||
}
|
||||
|
||||
@@ -270,14 +266,19 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
|
||||
return nil, errors.Wrap(err, "failed to get app info")
|
||||
}
|
||||
resp.Body.Close()
|
||||
location := resp.Request.URL
|
||||
if !strings.Contains(location.Path, AccessLoginWorkerPath) {
|
||||
return nil, fmt.Errorf("failed to get Access app info for %s", reqURL.String())
|
||||
}
|
||||
|
||||
aud := resp.Request.URL.Query().Get("kid")
|
||||
if aud == "" {
|
||||
return nil, errors.New("Empty app aud")
|
||||
var aud string
|
||||
location := resp.Request.URL
|
||||
if strings.Contains(location.Path, AccessLoginWorkerPath) {
|
||||
aud = resp.Request.URL.Query().Get("kid")
|
||||
if aud == "" {
|
||||
return nil, errors.New("Empty app aud")
|
||||
}
|
||||
} else if audHeader := resp.Header.Get(appAUDHeader); audHeader != "" {
|
||||
// 403/401 from the edge will have aud in a header
|
||||
aud = audHeader
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to find Access application at %s", reqURL.String())
|
||||
}
|
||||
|
||||
domain := resp.Header.Get(appDomainHeader)
|
||||
@@ -286,7 +287,6 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
|
||||
}
|
||||
|
||||
return &AppInfo{location.Hostname(), aud, domain}, nil
|
||||
|
||||
}
|
||||
|
||||
// exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
|
||||
@@ -341,7 +341,7 @@ func GetOrgTokenIfExists(authDomain string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var payload orgJWTPayload
|
||||
var payload jwtPayload
|
||||
err = json.Unmarshal(token.Payload, &payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -363,7 +363,7 @@ func GetAppTokenIfExists(appInfo *AppInfo) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var payload appJWTPayload
|
||||
var payload jwtPayload
|
||||
err = json.Unmarshal(token.Payload, &payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+31
-8
@@ -72,6 +72,7 @@ type Route interface {
|
||||
json.Marshaler
|
||||
RecordType() string
|
||||
UnmarshalResult(body io.Reader) (RouteResult, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type RouteResult interface {
|
||||
@@ -80,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)
|
||||
}
|
||||
@@ -116,6 +122,10 @@ func (dr *DNSRoute) RecordType() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) String() string {
|
||||
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
||||
}
|
||||
|
||||
func (res *DNSRouteResult) SuccessSummary() string {
|
||||
var msgFmt string
|
||||
switch res.CName {
|
||||
@@ -126,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 {
|
||||
@@ -164,6 +183,10 @@ func (lr *LBRoute) RecordType() string {
|
||||
return "lb"
|
||||
}
|
||||
|
||||
func (lb *LBRoute) String() string {
|
||||
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
|
||||
var result LBRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2017 Uber Technologies, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// +build linux
|
||||
|
||||
package cgroups
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// CGroup represents the data structure for a Linux control group.
|
||||
type CGroup struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// NewCGroup returns a new *CGroup from a given path.
|
||||
func NewCGroup(path string) *CGroup {
|
||||
return &CGroup{path: path}
|
||||
}
|
||||
|
||||
// Path returns the path of the CGroup*.
|
||||
func (cg *CGroup) Path() string {
|
||||
return cg.path
|
||||
}
|
||||
|
||||
// ParamPath returns the path of the given cgroup param under itself.
|
||||
func (cg *CGroup) ParamPath(param string) string {
|
||||
return filepath.Join(cg.path, param)
|
||||
}
|
||||
|
||||
// readFirstLine reads the first line from a cgroup param file.
|
||||
func (cg *CGroup) readFirstLine(param string) (string, error) {
|
||||
paramFile, err := os.Open(cg.ParamPath(param))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer paramFile.Close()
|
||||
|
||||
scanner := bufio.NewScanner(paramFile)
|
||||
if scanner.Scan() {
|
||||
return scanner.Text(), nil
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
// readInt parses the first line from a cgroup param file as int.
|
||||
func (cg *CGroup) readInt(param string) (int, error) {
|
||||
text, err := cg.readFirstLine(param)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(text)
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// +build linux
|
||||
|
||||
package cgroups
|
||||
|
||||
const (
|
||||
// _cgroupFSType is the Linux CGroup file system type used in
|
||||
// `/proc/$PID/mountinfo`.
|
||||
_cgroupFSType = "cgroup"
|
||||
// _cgroupSubsysCPU is the CPU CGroup subsystem.
|
||||
_cgroupSubsysCPU = "cpu"
|
||||
// _cgroupSubsysCPUAcct is the CPU accounting CGroup subsystem.
|
||||
_cgroupSubsysCPUAcct = "cpuacct"
|
||||
// _cgroupSubsysCPUSet is the CPUSet CGroup subsystem.
|
||||
_cgroupSubsysCPUSet = "cpuset"
|
||||
// _cgroupSubsysMemory is the Memory CGroup subsystem.
|
||||
_cgroupSubsysMemory = "memory"
|
||||
|
||||
// _cgroupCPUCFSQuotaUsParam is the file name for the CGroup CFS quota
|
||||
// parameter.
|
||||
_cgroupCPUCFSQuotaUsParam = "cpu.cfs_quota_us"
|
||||
// _cgroupCPUCFSPeriodUsParam is the file name for the CGroup CFS period
|
||||
// parameter.
|
||||
_cgroupCPUCFSPeriodUsParam = "cpu.cfs_period_us"
|
||||
)
|
||||
|
||||
const (
|
||||
_procPathCGroup = "/proc/self/cgroup"
|
||||
_procPathMountInfo = "/proc/self/mountinfo"
|
||||
)
|
||||
|
||||
// CGroups is a map that associates each CGroup with its subsystem name.
|
||||
type CGroups map[string]*CGroup
|
||||
|
||||
// NewCGroups returns a new *CGroups from given `mountinfo` and `cgroup` files
|
||||
// under for some process under `/proc` file system (see also proc(5) for more
|
||||
// information).
|
||||
func NewCGroups(procPathMountInfo, procPathCGroup string) (CGroups, error) {
|
||||
cgroupSubsystems, err := parseCGroupSubsystems(procPathCGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cgroups := make(CGroups)
|
||||
newMountPoint := func(mp *MountPoint) error {
|
||||
if mp.FSType != _cgroupFSType {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, opt := range mp.SuperOptions {
|
||||
subsys, exists := cgroupSubsystems[opt]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
cgroupPath, err := mp.Translate(subsys.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cgroups[opt] = NewCGroup(cgroupPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := parseMountInfo(procPathMountInfo, newMountPoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cgroups, nil
|
||||
}
|
||||
|
||||
// NewCGroupsForCurrentProcess returns a new *CGroups instance for the current
|
||||
// process.
|
||||
func NewCGroupsForCurrentProcess() (CGroups, error) {
|
||||
return NewCGroups(_procPathMountInfo, _procPathCGroup)
|
||||
}
|
||||
|
||||
// CPUQuota returns the CPU quota applied with the CPU cgroup controller.
|
||||
// It is a result of `cpu.cfs_quota_us / cpu.cfs_period_us`. If the value of
|
||||
// `cpu.cfs_quota_us` was not set (-1), the method returns `(-1, nil)`.
|
||||
func (cg CGroups) CPUQuota() (float64, bool, error) {
|
||||
cpuCGroup, exists := cg[_cgroupSubsysCPU]
|
||||
if !exists {
|
||||
return -1, false, nil
|
||||
}
|
||||
|
||||
cfsQuotaUs, err := cpuCGroup.readInt(_cgroupCPUCFSQuotaUsParam)
|
||||
if defined := cfsQuotaUs > 0; err != nil || !defined {
|
||||
return -1, defined, err
|
||||
}
|
||||
|
||||
cfsPeriodUs, err := cpuCGroup.readInt(_cgroupCPUCFSPeriodUsParam)
|
||||
if err != nil {
|
||||
return -1, false, err
|
||||
}
|
||||
|
||||
return float64(cfsQuotaUs) / float64(cfsPeriodUs), true, nil
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// Package cgroups provides utilities to access Linux control group (CGroups)
|
||||
// parameters (CPU quota, for example) for a given process.
|
||||
package cgroups
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// +build linux
|
||||
|
||||
package cgroups
|
||||
|
||||
import "fmt"
|
||||
|
||||
type cgroupSubsysFormatInvalidError struct {
|
||||
line string
|
||||
}
|
||||
|
||||
type mountPointFormatInvalidError struct {
|
||||
line string
|
||||
}
|
||||
|
||||
type pathNotExposedFromMountPointError struct {
|
||||
mountPoint string
|
||||
root string
|
||||
path string
|
||||
}
|
||||
|
||||
func (err cgroupSubsysFormatInvalidError) Error() string {
|
||||
return fmt.Sprintf("invalid format for CGroupSubsys: %q", err.line)
|
||||
}
|
||||
|
||||
func (err mountPointFormatInvalidError) Error() string {
|
||||
return fmt.Sprintf("invalid format for MountPoint: %q", err.line)
|
||||
}
|
||||
|
||||
func (err pathNotExposedFromMountPointError) Error() string {
|
||||
return fmt.Sprintf("path %q is not a descendant of mount point root %q and cannot be exposed from %q", err.path, err.root, err.mountPoint)
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// +build linux
|
||||
|
||||
package cgroups
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
_mountInfoSep = " "
|
||||
_mountInfoOptsSep = ","
|
||||
_mountInfoOptionalFieldsSep = "-"
|
||||
)
|
||||
|
||||
const (
|
||||
_miFieldIDMountID = iota
|
||||
_miFieldIDParentID
|
||||
_miFieldIDDeviceID
|
||||
_miFieldIDRoot
|
||||
_miFieldIDMountPoint
|
||||
_miFieldIDOptions
|
||||
_miFieldIDOptionalFields
|
||||
|
||||
_miFieldCountFirstHalf
|
||||
)
|
||||
|
||||
const (
|
||||
_miFieldOffsetFSType = iota
|
||||
_miFieldOffsetMountSource
|
||||
_miFieldOffsetSuperOptions
|
||||
|
||||
_miFieldCountSecondHalf
|
||||
)
|
||||
|
||||
const _miFieldCountMin = _miFieldCountFirstHalf + _miFieldCountSecondHalf
|
||||
|
||||
// MountPoint is the data structure for the mount points in
|
||||
// `/proc/$PID/mountinfo`. See also proc(5) for more information.
|
||||
type MountPoint struct {
|
||||
MountID int
|
||||
ParentID int
|
||||
DeviceID string
|
||||
Root string
|
||||
MountPoint string
|
||||
Options []string
|
||||
OptionalFields []string
|
||||
FSType string
|
||||
MountSource string
|
||||
SuperOptions []string
|
||||
}
|
||||
|
||||
// NewMountPointFromLine parses a line read from `/proc/$PID/mountinfo` and
|
||||
// returns a new *MountPoint.
|
||||
func NewMountPointFromLine(line string) (*MountPoint, error) {
|
||||
fields := strings.Split(line, _mountInfoSep)
|
||||
|
||||
if len(fields) < _miFieldCountMin {
|
||||
return nil, mountPointFormatInvalidError{line}
|
||||
}
|
||||
|
||||
mountID, err := strconv.Atoi(fields[_miFieldIDMountID])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentID, err := strconv.Atoi(fields[_miFieldIDParentID])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, field := range fields[_miFieldIDOptionalFields:] {
|
||||
if field == _mountInfoOptionalFieldsSep {
|
||||
fsTypeStart := _miFieldIDOptionalFields + i + 1
|
||||
|
||||
if len(fields) != fsTypeStart+_miFieldCountSecondHalf {
|
||||
return nil, mountPointFormatInvalidError{line}
|
||||
}
|
||||
|
||||
miFieldIDFSType := _miFieldOffsetFSType + fsTypeStart
|
||||
miFieldIDMountSource := _miFieldOffsetMountSource + fsTypeStart
|
||||
miFieldIDSuperOptions := _miFieldOffsetSuperOptions + fsTypeStart
|
||||
|
||||
return &MountPoint{
|
||||
MountID: mountID,
|
||||
ParentID: parentID,
|
||||
DeviceID: fields[_miFieldIDDeviceID],
|
||||
Root: fields[_miFieldIDRoot],
|
||||
MountPoint: fields[_miFieldIDMountPoint],
|
||||
Options: strings.Split(fields[_miFieldIDOptions], _mountInfoOptsSep),
|
||||
OptionalFields: fields[_miFieldIDOptionalFields:(fsTypeStart - 1)],
|
||||
FSType: fields[miFieldIDFSType],
|
||||
MountSource: fields[miFieldIDMountSource],
|
||||
SuperOptions: strings.Split(fields[miFieldIDSuperOptions], _mountInfoOptsSep),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, mountPointFormatInvalidError{line}
|
||||
}
|
||||
|
||||
// Translate converts an absolute path inside the *MountPoint's file system to
|
||||
// the host file system path in the mount namespace the *MountPoint belongs to.
|
||||
func (mp *MountPoint) Translate(absPath string) (string, error) {
|
||||
relPath, err := filepath.Rel(mp.Root, absPath)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if relPath == ".." || strings.HasPrefix(relPath, "../") {
|
||||
return "", pathNotExposedFromMountPointError{
|
||||
mountPoint: mp.MountPoint,
|
||||
root: mp.Root,
|
||||
path: absPath,
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Join(mp.MountPoint, relPath), nil
|
||||
}
|
||||
|
||||
// parseMountInfo parses procPathMountInfo (usually at `/proc/$PID/mountinfo`)
|
||||
// and yields parsed *MountPoint into newMountPoint.
|
||||
func parseMountInfo(procPathMountInfo string, newMountPoint func(*MountPoint) error) error {
|
||||
mountInfoFile, err := os.Open(procPathMountInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mountInfoFile.Close()
|
||||
|
||||
scanner := bufio.NewScanner(mountInfoFile)
|
||||
|
||||
for scanner.Scan() {
|
||||
mountPoint, err := NewMountPointFromLine(scanner.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := newMountPoint(mountPoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// +build linux
|
||||
|
||||
package cgroups
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
_cgroupSep = ":"
|
||||
_cgroupSubsysSep = ","
|
||||
)
|
||||
|
||||
const (
|
||||
_csFieldIDID = iota
|
||||
_csFieldIDSubsystems
|
||||
_csFieldIDName
|
||||
_csFieldCount
|
||||
)
|
||||
|
||||
// CGroupSubsys represents the data structure for entities in
|
||||
// `/proc/$PID/cgroup`. See also proc(5) for more information.
|
||||
type CGroupSubsys struct {
|
||||
ID int
|
||||
Subsystems []string
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewCGroupSubsysFromLine returns a new *CGroupSubsys by parsing a string in
|
||||
// the format of `/proc/$PID/cgroup`
|
||||
func NewCGroupSubsysFromLine(line string) (*CGroupSubsys, error) {
|
||||
fields := strings.SplitN(line, _cgroupSep, _csFieldCount)
|
||||
|
||||
if len(fields) != _csFieldCount {
|
||||
return nil, cgroupSubsysFormatInvalidError{line}
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(fields[_csFieldIDID])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cgroup := &CGroupSubsys{
|
||||
ID: id,
|
||||
Subsystems: strings.Split(fields[_csFieldIDSubsystems], _cgroupSubsysSep),
|
||||
Name: fields[_csFieldIDName],
|
||||
}
|
||||
|
||||
return cgroup, nil
|
||||
}
|
||||
|
||||
// parseCGroupSubsystems parses procPathCGroup (usually at `/proc/$PID/cgroup`)
|
||||
// and returns a new map[string]*CGroupSubsys.
|
||||
func parseCGroupSubsystems(procPathCGroup string) (map[string]*CGroupSubsys, error) {
|
||||
cgroupFile, err := os.Open(procPathCGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cgroupFile.Close()
|
||||
|
||||
scanner := bufio.NewScanner(cgroupFile)
|
||||
subsystems := make(map[string]*CGroupSubsys)
|
||||
|
||||
for scanner.Scan() {
|
||||
cgroup, err := NewCGroupSubsysFromLine(scanner.Text())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, subsys := range cgroup.Subsystems {
|
||||
subsystems[subsys] = cgroup
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return subsystems, nil
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// +build linux
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
cg "go.uber.org/automaxprocs/internal/cgroups"
|
||||
)
|
||||
|
||||
// CPUQuotaToGOMAXPROCS converts the CPU quota applied to the calling process
|
||||
// to a valid GOMAXPROCS value.
|
||||
func CPUQuotaToGOMAXPROCS(minValue int) (int, CPUQuotaStatus, error) {
|
||||
cgroups, err := cg.NewCGroupsForCurrentProcess()
|
||||
if err != nil {
|
||||
return -1, CPUQuotaUndefined, err
|
||||
}
|
||||
|
||||
quota, defined, err := cgroups.CPUQuota()
|
||||
if !defined || err != nil {
|
||||
return -1, CPUQuotaUndefined, err
|
||||
}
|
||||
|
||||
maxProcs := int(math.Floor(quota))
|
||||
if minValue > 0 && maxProcs < minValue {
|
||||
return minValue, CPUQuotaMinUsed, nil
|
||||
}
|
||||
return maxProcs, CPUQuotaUsed, nil
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package runtime
|
||||
|
||||
// CPUQuotaToGOMAXPROCS converts the CPU quota applied to the calling process
|
||||
// to a valid GOMAXPROCS value. This is Linux-specific and not supported in the
|
||||
// current OS.
|
||||
func CPUQuotaToGOMAXPROCS(_ int) (int, CPUQuotaStatus, error) {
|
||||
return -1, CPUQuotaUndefined, nil
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
package runtime
|
||||
|
||||
// CPUQuotaStatus presents the status of how CPU quota is used
|
||||
type CPUQuotaStatus int
|
||||
|
||||
const (
|
||||
// CPUQuotaUndefined is returned when CPU quota is undefined
|
||||
CPUQuotaUndefined CPUQuotaStatus = iota
|
||||
// CPUQuotaUsed is returned when a valid CPU quota can be used
|
||||
CPUQuotaUsed
|
||||
// CPUQuotaMinUsed is return when CPU quota is smaller than the min value
|
||||
CPUQuotaMinUsed
|
||||
)
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// Package maxprocs lets Go programs easily configure runtime.GOMAXPROCS to
|
||||
// match the configured Linux CPU quota. Unlike the top-level automaxprocs
|
||||
// package, it lets the caller configure logging and handle errors.
|
||||
package maxprocs // import "go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
iruntime "go.uber.org/automaxprocs/internal/runtime"
|
||||
)
|
||||
|
||||
const _maxProcsKey = "GOMAXPROCS"
|
||||
|
||||
func currentMaxProcs() int {
|
||||
return runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
printf func(string, ...interface{})
|
||||
procs func(int) (int, iruntime.CPUQuotaStatus, error)
|
||||
minGOMAXPROCS int
|
||||
}
|
||||
|
||||
func (c *config) log(fmt string, args ...interface{}) {
|
||||
if c.printf != nil {
|
||||
c.printf(fmt, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// An Option alters the behavior of Set.
|
||||
type Option interface {
|
||||
apply(*config)
|
||||
}
|
||||
|
||||
// Logger uses the supplied printf implementation for log output. By default,
|
||||
// Set doesn't log anything.
|
||||
func Logger(printf func(string, ...interface{})) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
cfg.printf = printf
|
||||
})
|
||||
}
|
||||
|
||||
// Min sets the minimum GOMAXPROCS value that will be used.
|
||||
// Any value below 1 is ignored.
|
||||
func Min(n int) Option {
|
||||
return optionFunc(func(cfg *config) {
|
||||
if n >= 1 {
|
||||
cfg.minGOMAXPROCS = n
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type optionFunc func(*config)
|
||||
|
||||
func (of optionFunc) apply(cfg *config) { of(cfg) }
|
||||
|
||||
// Set GOMAXPROCS to match the Linux container CPU quota (if any), returning
|
||||
// any error encountered and an undo function.
|
||||
//
|
||||
// Set is a no-op on non-Linux systems and in Linux environments without a
|
||||
// configured CPU quota.
|
||||
func Set(opts ...Option) (func(), error) {
|
||||
cfg := &config{
|
||||
procs: iruntime.CPUQuotaToGOMAXPROCS,
|
||||
minGOMAXPROCS: 1,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o.apply(cfg)
|
||||
}
|
||||
|
||||
undoNoop := func() {
|
||||
cfg.log("maxprocs: No GOMAXPROCS change to reset")
|
||||
}
|
||||
|
||||
// Honor the GOMAXPROCS environment variable if present. Otherwise, amend
|
||||
// `runtime.GOMAXPROCS()` with the current process' CPU quota if the OS is
|
||||
// Linux, and guarantee a minimum value of 1. The minimum guaranteed value
|
||||
// can be overriden using `maxprocs.Min()`.
|
||||
if max, exists := os.LookupEnv(_maxProcsKey); exists {
|
||||
cfg.log("maxprocs: Honoring GOMAXPROCS=%q as set in environment", max)
|
||||
return undoNoop, nil
|
||||
}
|
||||
|
||||
maxProcs, status, err := cfg.procs(cfg.minGOMAXPROCS)
|
||||
if err != nil {
|
||||
return undoNoop, err
|
||||
}
|
||||
|
||||
if status == iruntime.CPUQuotaUndefined {
|
||||
cfg.log("maxprocs: Leaving GOMAXPROCS=%v: CPU quota undefined", currentMaxProcs())
|
||||
return undoNoop, nil
|
||||
}
|
||||
|
||||
prev := currentMaxProcs()
|
||||
undo := func() {
|
||||
cfg.log("maxprocs: Resetting GOMAXPROCS to %v", prev)
|
||||
runtime.GOMAXPROCS(prev)
|
||||
}
|
||||
|
||||
switch status {
|
||||
case iruntime.CPUQuotaMinUsed:
|
||||
cfg.log("maxprocs: Updating GOMAXPROCS=%v: using minimum allowed GOMAXPROCS", maxProcs)
|
||||
case iruntime.CPUQuotaUsed:
|
||||
cfg.log("maxprocs: Updating GOMAXPROCS=%v: determined from CPU quota", maxProcs)
|
||||
}
|
||||
|
||||
runtime.GOMAXPROCS(maxProcs)
|
||||
return undo, nil
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
package maxprocs
|
||||
|
||||
// Version is the current package version.
|
||||
const Version = "1.4.0"
|
||||
Vendored
+5
@@ -250,6 +250,11 @@ github.com/stretchr/testify/require
|
||||
## explicit
|
||||
github.com/urfave/cli/v2
|
||||
github.com/urfave/cli/v2/altsrc
|
||||
# go.uber.org/automaxprocs v1.4.0
|
||||
## explicit
|
||||
go.uber.org/automaxprocs/internal/cgroups
|
||||
go.uber.org/automaxprocs/internal/runtime
|
||||
go.uber.org/automaxprocs/maxprocs
|
||||
# golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
|
||||
## explicit
|
||||
golang.org/x/crypto/blake2b
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user