Compare commits

...

34 Commits

Author SHA1 Message Date
Sudarsan Reddy bcc847963e Release 2021.8.0 2021-08-02 09:28:51 +01:00
Nuno Diegues 1e8dea9112 TUN-4851: Component tests to sanity check that Proxy DNS and Tunnel are only run when expected 2021-08-02 08:01:09 +00:00
Sudarsan Reddy cd4af5696d TUN-4772: Release built executables with packages 2021-07-30 13:56:19 +00:00
Nuno Diegues 8527d03a29 TUN-4847: Allow to list tunnels by prefix name or exclusion prefix name 2021-07-30 12:00:26 +01:00
Nuno Diegues fa8aa02270 TUN-4819: Tolerate protocol TXT record lookup failing 2021-07-29 11:44:39 +00:00
Nuno Diegues aa24338225 TUN-4832: Prevent tunnel from running accidentally when only proxy-dns should run 2021-07-29 11:05:12 +00:00
Nuno Diegues 0924549efd TUN-4811: Publish quick tunnels' hostname in /metrics under userHostname for backwards-compatibility 2021-07-29 10:20:43 +01:00
Sudarsan Reddy 67a3be5b7a Release 2021.7.4 2021-07-28 10:35:17 +01:00
Sudarsan Reddy c33a516663 TUN-4812: Disable CGO for cloudflared builds 2021-07-28 09:33:59 +00:00
Sudarsan Reddy ed1389ef08 TUN-4814: Revert "TUN-4699: Make quick tunnels the default in cloudflared"
This reverts commit 18992efa0c.
2021-07-28 10:02:55 +01:00
Sudarsan Reddy 8fb6508ae6 Release 2021.7.3 2021-07-27 18:00:03 +01:00
Sudarsan Reddy ee8c8bd4c6 TUN-4799: Build deb, msi and rpm packages with fips 2021-07-27 11:38:22 +01:00
Sudarsan Reddy 0269062b24 Release 2021.7.2 2021-07-27 10:01:39 +01:00
Sudarsan Reddy bccf650165 Fixed a syntax error with python logging. 2021-07-27 09:33:41 +01:00
Sudarsan Reddy cecc74d3fb Release 2021.7.1 2021-07-26 17:19:43 +01:00
Rishabh Bector 18992efa0c TUN-4699: Make quick tunnels the default in cloudflared 2021-07-26 15:57:36 +00:00
Sudarsan Reddy bd8af7d80d TUN-4771: Upload deb, rpm and msi packages to github
- cfsetup now has a build command `github-release-pkgs` to release linux
   and msi packages to github.
 - github_message.py now has an option to upload all assets in a provided
   directory.
2021-07-26 13:49:17 +01:00
Sudarsan Reddy dff694b218 TUN-4761: Added a build-all-packages target to cfsetup 2021-07-22 16:36:49 +01:00
Sudarsan Reddy 38af26e232 TUN-4755: Add a windows msi release option to Make 2021-07-21 10:31:16 +01:00
Sudarsan Reddy 8f3526289a TUN-4701: Split Proxy into ProxyHTTP and ProxyTCP
http.Request now is only used by ProxyHTTP and not required if the
proxying is TCP. The dest conversion is handled by the transport layer.
2021-07-19 13:43:59 +00:00
Sudarsan Reddy 81dff44bb9 TUN-4596: Add QUIC application protocol for QUIC stream handshake
- Vendored the capnproto library to cloudflared.
- Added capnproto schema defining application protocol.
- Added Pogs and application level read write of the protocol.
2021-07-15 17:35:25 +00:00
cthuang 6e45e0d53b TUN-4714: Name nightly package cloudflared-nightly to avoid apt conflict 2021-07-14 20:45:29 +01:00
Sudarsan Reddy a7d2de1e12 Release 2021.7.0 2021-07-12 10:29:22 +01:00
Rishabh Bector 59cae0f622 TUN-4698: Add cloudflared metrics endpoint to serve quick tunnel hostname 2021-07-12 09:26:07 +00:00
Sudarsan Reddy d678584d89 TUN-4655: ingress.StreamBasedProxy.EstablishConnection takes dest input
This change extracts the need for EstablishConnection to know about a
request's entire context. It also removes the concern of populating the
http.Response from EstablishConnection's responsibilities.
2021-07-07 15:58:04 +01:00
Sudarsan Reddy f1b57526b3 TUN-4626: Proxy non-stream based origin websockets with http Roundtrip.
Reuses HTTPProxy's Roundtrip method to directly proxy websockets from
eyeball clients (determined by websocket type and ingress not being
connection oriented , i.e. Not ssh or smb for example) to proxy
websocket traffic.
2021-07-07 14:31:42 +00:00
Rishabh Bector 3eb9efd9f0 TUN-4521: Modify cloudflared to use zoneless-tunnels-worker for free tunnels 2021-06-29 09:39:18 +01:00
Michael Borkenstein 8d99e92852 AUTH-3475: Updated GetAppInfo error message 2021-06-25 10:37:48 -05:00
Nuno Diegues 31ff7caeeb Release 2021.6.0 2021-06-21 14:43:35 +01:00
Nuno Diegues 5d84874d53 TUN-4571: Changelog for 2021.6.0 2021-06-21 12:04:05 +01:00
Nuno Diegues b06fe0fc5f TUN-4571: Fix proxying to unix sockets when using HTTP2 transport to Cloudflare Edge 2021-06-21 11:43:14 +01:00
Nuno Diegues bf068e728f TUN-4504: Fix component tests in windows 2021-06-08 19:34:42 +01:00
Nuno Diegues f88732277a TUN-4502: Make cloudflared tunnel route subcommands described consistently 2021-06-07 09:20:11 +01:00
Nuno Diegues 98a0844f56 TUN-4461: Log resulting DNS hostname if one is received from Cloudflare API 2021-05-27 17:32:16 +01:00
49 changed files with 1808 additions and 629 deletions
+1
View File
@@ -15,3 +15,4 @@ cscope.*
*-session.log
ssh_server_tests/.env
/.cover
built_artifacts/
+14 -3
View File
@@ -1,5 +1,15 @@
**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.
@@ -10,9 +20,10 @@
## 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 when using the CLI to provision DNS, and when starting an adhoc tunnel, e.g.
- `cloudflared route --overwrite-dns dns foo foo.example.com`
- `cloudflared tunnel --overwrite-dns --name foo --hostname foo.example.com`
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
+29 -9
View File
@@ -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
+41
View File
@@ -1,3 +1,44 @@
2021.8.0
- 2021-07-30 TUN-4847: Allow to list tunnels by prefix name or exclusion prefix name
- 2021-07-30 TUN-4772: Release built executables with packages
- 2021-07-30 TUN-4851: Component tests to sanity check that Proxy DNS and Tunnel are only run when expected
- 2021-07-28 TUN-4811: Publish quick tunnels' hostname in /metrics under `userHostname` for backwards-compatibility
- 2021-07-29 TUN-4832: Prevent tunnel from running accidentally when only proxy-dns should run
- 2021-07-28 TUN-4819: Tolerate protocol TXT record lookup failing
2021.7.4
- 2021-07-28 TUN-4814: Revert "TUN-4699: Make quick tunnels the default in cloudflared"
- 2021-07-28 TUN-4812: Disable CGO for cloudflared builds
2021.7.3
- 2021-07-27 TUN-4799: Build deb, msi and rpm packages with fips
2021.7.2
- 2021-07-27 Fixed a syntax error with python logging.
2021.7.1
- 2021-07-21 TUN-4755: Add a windows msi release option to Make
- 2021-07-22 TUN-4761: Added a build-all-packages target to cfsetup
- 2021-07-26 TUN-4771: Upload deb, rpm and msi packages to github
- 2021-07-14 TUN-4714: Name nightly package cloudflared-nightly to avoid apt conflict
- 2021-07-16 TUN-4701: Split Proxy into ProxyHTTP and ProxyTCP
- 2021-07-08 TUN-4596: Add QUIC application protocol for QUIC stream handshake
- 2021-07-09 TUN-4699: Make quick tunnels the default in cloudflared
2021.7.0
- 2021-07-01 TUN-4626: Proxy non-stream based origin websockets with http Roundtrip.
- 2021-07-01 TUN-4655: ingress.StreamBasedProxy.EstablishConnection takes dest input
- 2021-07-09 TUN-4698: Add cloudflared metrics endpoint to serve quick tunnel hostname
- 2021-06-21 TUN-4521: Modify cloudflared to use zoneless-tunnels-worker for free tunnels
- 2021-04-05 AUTH-3475: Updated GetAppInfo error message
2021.6.0
- 2021-06-21 TUN-4571: Changelog for 2021.6.0
- 2021-06-18 TUN-4571: Fix proxying to unix sockets when using HTTP2 transport to Cloudflare Edge
- 2021-06-07 TUN-4502: Make `cloudflared tunnel route` subcommands described consistently
- 2021-06-08 TUN-4504: Fix component tests in windows
- 2021-05-27 TUN-4461: Log resulting DNS hostname if one is received from Cloudflare API
2021.5.10
- 2021-05-25 TUN-4456: Replaced instances of Tick() with Ticker() in h2mux paths
+37
View File
@@ -0,0 +1,37 @@
VERSION=$(git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
echo $VERSION
export CGO_ENABLED=0
# This controls the directory the built artifacts go into
export ARTIFACT_DIR=built_artifacts/
mkdir -p $ARTIFACT_DIR
windowsArchs=("amd64" "386")
export TARGET_OS=windows
for arch in ${windowsArchs[@]}; do
export TARGET_ARCH=$arch
make cloudflared-msi
mv ./cloudflared.exe $ARTIFACT_DIR/cloudflared-windows-$arch.exe
mv cloudflared-$VERSION-$arch.msi $ARTIFACT_DIR/cloudflared-windows-$arch.msi
done
export FIPS=true
linuxArchs=("amd64" "386" "arm")
export TARGET_OS=linux
for arch in ${linuxArchs[@]}; do
export TARGET_ARCH=$arch
make cloudflared-deb
mv cloudflared\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-linux-$arch.deb
# rpm packages invert the - and _ and use x86_64 instead of amd64.
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g')
RPMARCH=$arch
if [ $arch == "amd64" ];then
RPMARCH="x86_64"
fi
make cloudflared-rpm
mv cloudflared-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-linux-$RPMARCH.rpm
# finally move the linux binary as well.
mv ./cloudflared $ARTIFACT_DIR/cloudflared-linux-$arch
done
+56 -87
View File
@@ -14,25 +14,73 @@ 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:
- export FIPS=true
- ./build-packages.sh
github-release-pkgs:
build_dir: *build_dir
builddeps:
- *pinned_go_fips
- build-essential
- fakeroot
- rubygem-fpm
- rpm
- wget
# libmsi and libgcab are libraries the wixl binary depends on.
- libmsi-dev
- libgcab-dev
- python3-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
@@ -60,20 +108,6 @@ stretch: &stretch
- export GOARCH=amd64
- export FIPS=true
- make release
github-release-linux-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go_fips
- build-essential
- python3-setuptools
- python3-pip
pre-cache: &install_pygithub
- pip3 install pygithub
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- export FIPS=true
- make github-release
release-linux-armv6:
build_dir: *build_dir
builddeps:
@@ -85,20 +119,6 @@ stretch: &stretch
- export GOARCH=arm
- export CC=arm-linux-gnueabihf-gcc
- make release
github-release-linux-armv6:
build_dir: *build_dir
builddeps:
- *pinned_go
- crossbuild-essential-armhf
- gcc-arm-linux-gnueabihf
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- export GOOS=linux
- export GOARCH=arm
- export CC=arm-linux-gnueabihf-gcc
- make github-release
release-linux-386:
build_dir: *build_dir
builddeps:
@@ -108,18 +128,6 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=386
- make release
github-release-linux-386:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-multilib
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- export GOOS=linux
- export GOARCH=386
- make github-release
release-windows-amd64:
build_dir: *build_dir
builddeps:
@@ -130,19 +138,6 @@ stretch: &stretch
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
- make release
github-release-windows-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-mingw-w64
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- export GOOS=windows
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
- make github-release
release-windows-386:
build_dir: *build_dir
builddeps:
@@ -153,40 +148,14 @@ stretch: &stretch
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
- make release
github-release-windows-386:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-mingw-w64
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- export GOOS=windows
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
- make github-release
github-release-linux-arm64:
build_dir: *build_dir
builddeps:
- *pinned_go
- crossbuild-essential-armhf
- g++-aarch64-linux-gnu
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
post-cache:
- export GOOS=linux
- export GOARCH=arm64
- export CC=aarch64-linux-gnu-gcc
- make github-release
github-release-macos-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- python3-setuptools
- python3-pip
pre-cache: *install_pygithub
pre-cache: &install_pygithub
- pip3 install pygithub
post-cache:
- make github-mac-upload
test:
+62
View File
@@ -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>
+1 -1
View File
@@ -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"),
+14 -2
View File
@@ -163,6 +163,12 @@ func TunnelCommand(c *cli.Context) error {
return fmt.Errorf("Use `cloudflared tunnel run` to start tunnel %s", ref)
}
// Unauthenticated named tunnel on <random>.<quick-tunnels-service>.com
// For now, default to legacy setup unless quick-service is specified
if !dnsProxyStandAlone(c, nil) && c.String("hostname") == "" && c.String("quick-service") != "" {
return RunQuickTunnel(sc)
}
// Start a classic tunnel
return runClassicTunnel(sc)
}
@@ -200,7 +206,7 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri
// runClassicTunnel creates a "classic" non-named tunnel
func runClassicTunnel(sc *subcommandContext) error {
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled, "")
}
func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) {
@@ -219,6 +225,7 @@ func StartServer(
namedTunnel *connection.NamedTunnelConfig,
log *zerolog.Logger,
isUIEnabled bool,
quickTunnelHostname string,
) error {
_ = raven.SetDSN(sentryDSN)
var wg sync.WaitGroup
@@ -335,7 +342,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, quickTunnelHostname, log)
}()
if err := ingressRules.StartOrigins(&wg, log, ctx.Done(), errC); err != nil {
@@ -616,6 +623,11 @@ 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.",
Hidden: true,
}),
selectProtocolFlag,
overwriteDNSFlag,
}...)
+94
View File
@@ -0,0 +1,94 @@
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,
}
url := data.Result.Hostname
if !strings.HasPrefix(url, "https://") {
url = "https://" + url
}
for _, line := range connection.AsciiBox([]string{
"Your Quick Tunnel has been created! Visit it at:",
url,
}, 2) {
sc.log.Info().Msg(line)
}
return StartServer(
sc.c,
version,
&connection.NamedTunnelConfig{Credentials: credentials},
sc.log,
sc.isUIEnabled,
data.Result.Hostname,
)
}
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"`
}
@@ -286,6 +286,7 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
&connection.NamedTunnelConfig{Credentials: credentials},
sc.log,
sc.isUIEnabled,
"",
)
}
+59 -30
View File
@@ -49,6 +49,16 @@ var (
Aliases: []string{"n"},
Usage: "List tunnels with the given `NAME`",
}
listNamePrefixFlag = &cli.StringFlag{
Name: "name-prefix",
Aliases: []string{"np"},
Usage: "List tunnels that start with the give `NAME` prefix",
}
listExcludeNamePrefixFlag = &cli.StringFlag{
Name: "exclude-name-prefix",
Aliases: []string{"enp"},
Usage: "List tunnels whose `NAME` does not start with the given prefix",
}
listExistedAtFlag = &cli.TimestampFlag{
Name: "when",
Aliases: []string{"w"},
@@ -217,6 +227,8 @@ func buildListCommand() *cli.Command {
outputFormatFlag,
showDeletedFlag,
listNameFlag,
listNamePrefixFlag,
listExcludeNamePrefixFlag,
listExistedAtFlag,
listIDFlag,
showRecentlyDisconnected,
@@ -243,6 +255,12 @@ func listCommand(c *cli.Context) error {
if name := c.String("name"); name != "" {
filter.ByName(name)
}
if namePrefix := c.String("name-prefix"); namePrefix != "" {
filter.ByNamePrefix(namePrefix)
}
if excludePrefix := c.String("exclude-name-prefix"); excludePrefix != "" {
filter.ExcludeNameWithPrefix(excludePrefix)
}
if existedAt := c.Timestamp("time"); existedAt != nil {
filter.ByExistedAt(*existedAt)
}
@@ -655,7 +673,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.
@@ -675,16 +692,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(),
},
Flags: []cli.Flag{overwriteDNSFlag},
}
}
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())
@@ -700,9 +731,9 @@ func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route,
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())
@@ -744,41 +775,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, c.Bool(overwriteDNSFlagName))
if err != nil {
return err
}
case "lb":
tunnelID, err = sc.findID(c.Args().Get(tunnelIDIndex))
if err != nil {
return err
}
route, err = lbRouteFromArg(c)
if err != nil {
return err
}
default:
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
}
if err != nil {
return err
}
res, err := sc.route(tunnelID, route)
+10 -1
View File
@@ -3,7 +3,7 @@ import copy
from dataclasses import dataclass, InitVar
from constants import METRICS_PORT
from constants import METRICS_PORT, PROXY_DNS_PORT
# frozen=True raises exception when assigning to fields. This emulates immutability
@@ -93,3 +93,12 @@ class ClassicTunnelConfig(ClassicTunnelBaseConfig):
def get_url(self):
return "https://" + self.hostname
@dataclass(frozen=True)
class ProxyDnsConfig(BaseConfig):
full_config = {
"port": PROXY_DNS_PORT,
"no-autoupdate": True,
}
+29 -14
View File
@@ -1,14 +1,21 @@
import os
from enum import Enum, auto
from time import sleep
import pytest
import yaml
from time import sleep
from config import NamedTunnelConfig, ClassicTunnelConfig
from constants import BACKOFF_SECS
from config import NamedTunnelConfig, ClassicTunnelConfig, ProxyDnsConfig
from constants import BACKOFF_SECS, PROXY_DNS_PORT
from util import LOGGER
class CfdModes(Enum):
NAMED = auto()
CLASSIC = auto()
PROXY_DNS = auto()
@pytest.fixture(scope="session")
def component_tests_config():
config_file = os.getenv("COMPONENT_TESTS_CONFIG")
@@ -19,22 +26,30 @@ def component_tests_config():
config = yaml.safe_load(stream)
LOGGER.info(f"component tests base config {config}")
def _component_tests_config(additional_config={}, named_tunnel=True):
def _component_tests_config(additional_config={}, cfd_mode=CfdModes.NAMED, run_proxy_dns=True):
if run_proxy_dns:
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running.
# So we run all tests with it.
additional_config["proxy-dns"] = True
additional_config["proxy-dns-port"] = PROXY_DNS_PORT
else:
additional_config.pop("proxy-dns", None)
additional_config.pop("proxy-dns-port", None)
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running
additional_config["proxy-dns"] = True
additional_config["proxy-dns-port"] = 9053
if named_tunnel:
if cfd_mode is CfdModes.NAMED:
return NamedTunnelConfig(additional_config=additional_config,
cloudflared_binary=config['cloudflared_binary'],
tunnel=config['tunnel'],
credentials_file=config['credentials_file'],
ingress=config['ingress'])
return ClassicTunnelConfig(
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'],
hostname=config['classic_hostname'], origincert=config['origincert'])
elif cfd_mode is CfdModes.CLASSIC:
return ClassicTunnelConfig(
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'],
hostname=config['classic_hostname'], origincert=config['origincert'])
elif cfd_mode is CfdModes.PROXY_DNS:
return ProxyDnsConfig(cloudflared_binary=config['cloudflared_binary'])
else:
raise Exception(f"Unknown cloudflared mode {cfd_mode}")
return _component_tests_config
+2
View File
@@ -1,3 +1,5 @@
METRICS_PORT = 51000
MAX_RETRIES = 5
BACKOFF_SECS = 7
PROXY_DNS_PORT = 9053
+1 -1
View File
@@ -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):
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python
import socket
from time import sleep
import constants
from conftest import CfdModes
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
# Sanity checks that test that we only run Proxy DNS and Tunnel when we really expect them to be there.
class TestProxyDns:
def test_proxy_dns_with_named_tunnel(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=True)
def test_proxy_dns_with_classic_tunnel(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=True)
def test_proxy_dns_alone(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.PROXY_DNS, run_proxy_dns=True)
def test_named_tunnel_alone(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.NAMED, run_proxy_dns=False)
def test_classic_tunnel_alone(self, tmp_path, component_tests_config):
run_test_scenario(tmp_path, component_tests_config, CfdModes.CLASSIC, run_proxy_dns=False)
def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns):
expect_proxy_dns = run_proxy_dns
expect_tunnel = False
if cfd_mode == CfdModes.NAMED:
expect_tunnel = True
pre_args = ["tunnel"]
args = ["run"]
elif cfd_mode == CfdModes.CLASSIC:
expect_tunnel = True
pre_args = []
args = []
elif cfd_mode == CfdModes.PROXY_DNS:
expect_proxy_dns = True
pre_args = []
args = ["proxy-dns", "--port", str(constants.PROXY_DNS_PORT)]
else:
assert False, f"Unknown cfd_mode {cfd_mode}"
config = component_tests_config(cfd_mode=cfd_mode, run_proxy_dns=run_proxy_dns)
with start_cloudflared(tmp_path, config, cfd_pre_args=pre_args, cfd_args=args, new_process=True, capture_output=False):
if expect_tunnel:
wait_tunnel_ready()
else:
check_tunnel_not_connected()
verify_proxy_dns(expect_proxy_dns)
def verify_proxy_dns(should_be_running):
# Wait for the Proxy DNS listener to come up.
sleep(constants.BACKOFF_SECS)
had_failure = False
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(('localhost', constants.PROXY_DNS_PORT))
sock.send(b"anything")
except:
if should_be_running:
assert False, "Expected Proxy DNS to be running, but it was not."
had_failure = True
finally:
sock.close()
if not should_be_running and not had_failure:
assert False, "Proxy DNS should not have been running, but it was."
+11 -10
View File
@@ -1,20 +1,24 @@
#!/usr/bin/env python
import copy
import platform
from time import sleep
import pytest
from flaky import flaky
from conftest import CfdModes
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
@flaky(max_runs=3, min_passes=1)
class TestReconnect():
class TestReconnect:
default_ha_conns = 4
default_reconnect_secs = 5
default_reconnect_secs = 15
extra_config = {
"stdin-control": True,
}
@pytest.mark.skipif(platform.system() == "Windows", reason=f"Currently buggy on Windows TUN-4584")
def test_named_reconnect(self, tmp_path, component_tests_config):
config = component_tests_config(self.extra_config)
with start_cloudflared(tmp_path, config, new_process=True, allow_input=True, capture_output=False) as cloudflared:
@@ -24,8 +28,7 @@ class TestReconnect():
def test_classic_reconnect(self, tmp_path, component_tests_config):
extra_config = copy.copy(self.extra_config)
extra_config["hello-world"] = True
config = component_tests_config(
additional_config=extra_config, named_tunnel=False)
config = component_tests_config(additional_config=extra_config, cfd_mode=CfdModes.CLASSIC)
with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=False) as cloudflared:
self.assert_reconnect(config, cloudflared, 1)
@@ -36,7 +39,7 @@ class TestReconnect():
cloudflared.stdin.flush()
def assert_reconnect(self, config, cloudflared, repeat):
wait_tunnel_ready(tunnel_url=config.get_url())
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
for _ in range(repeat):
for i in range(self.default_ha_conns):
self.send_reconnect(cloudflared, self.default_reconnect_secs)
@@ -44,11 +47,9 @@ class TestReconnect():
if expect_connections > 0:
# Don't check if tunnel returns 200 here because there is a race condition between wait_tunnel_ready
# retrying to get 200 response and reconnecting
wait_tunnel_ready(
require_min_connections=expect_connections)
wait_tunnel_ready(require_min_connections=expect_connections)
else:
check_tunnel_not_connected()
sleep(self.default_reconnect_secs + 10)
wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=self.default_ha_conns)
sleep(self.default_reconnect_secs * 2)
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=self.default_ha_conns)
+3 -2
View File
@@ -8,6 +8,7 @@ from pathlib import Path
import pytest
import test_logging
from conftest import CfdModes
from util import start_cloudflared, wait_tunnel_ready
@@ -34,7 +35,7 @@ class TestServiceMode:
"hello-world": True,
"logfile": str(log_file),
}
config = component_tests_config(additional_config=additional_config, named_tunnel=False)
config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC)
def assert_log_file():
test_logging.assert_log_in_file(log_file)
@@ -52,7 +53,7 @@ class TestServiceMode:
"loglevel": "debug",
"log-directory": str(log_dir),
}
config = component_tests_config(additional_config=additional_config, named_tunnel=False)
config = component_tests_config(additional_config=additional_config, cfd_mode=CfdModes.CLASSIC)
def assert_rotating_log():
test_logging.assert_log_to_dir(config, log_dir)
+2 -3
View File
@@ -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)
+73 -3
View File
@@ -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
@@ -87,9 +94,64 @@ func (t Type) String() string {
}
}
// OriginProxy is how data flows from cloudflared to the origin services running behind it.
type OriginProxy interface {
// If Proxy returns an error, the caller is responsible for writing the error status to ResponseWriter
Proxy(w ResponseWriter, req *http.Request, sourceConnectionType Type) error
ProxyHTTP(w ResponseWriter, req *http.Request, isWebsocket bool) error
ProxyTCP(ctx context.Context, rwa ReadWriteAcker, req *TCPRequest) error
}
// TCPRequest defines the input format needed to perform a TCP proxy.
type TCPRequest struct {
Dest string
CFRay string
LBProbe bool
}
// ReadWriteAcker is a readwriter with the ability to Acknowledge to the downstream (edge) that the origin has
// accepted the connection.
type ReadWriteAcker interface {
io.ReadWriter
AckConnection() error
}
// HTTPResponseReadWriteAcker is an HTTP implementation of ReadWriteAcker.
type HTTPResponseReadWriteAcker struct {
r io.Reader
w ResponseWriter
req *http.Request
}
// NewHTTPResponseReadWriterAcker returns a new instance of HTTPResponseReadWriteAcker.
func NewHTTPResponseReadWriterAcker(w ResponseWriter, req *http.Request) *HTTPResponseReadWriteAcker {
return &HTTPResponseReadWriteAcker{
r: req.Body,
w: w,
req: req,
}
}
func (h *HTTPResponseReadWriteAcker) Read(p []byte) (int, error) {
return h.r.Read(p)
}
func (h *HTTPResponseReadWriteAcker) Write(p []byte) (int, error) {
return h.w.Write(p)
}
// AckConnection acks an HTTP connection by sending a switch protocols status code that enables the caller to
// upgrade to streams.
func (h *HTTPResponseReadWriteAcker) AckConnection() error {
resp := &http.Response{
Status: switchingProtocolText,
StatusCode: http.StatusSwitchingProtocols,
ContentLength: -1,
}
if secWebsocketKey := h.req.Header.Get("Sec-WebSocket-Key"); secWebsocketKey != "" {
resp.Header = websocket.NewResponseHeader(h.req)
}
return h.w.WriteRespHeaders(resp.StatusCode, resp.Header)
}
type ResponseWriter interface {
@@ -112,3 +174,11 @@ func IsServerSentEvent(headers http.Header) bool {
func uint8ToString(input uint8) string {
return strconv.FormatUint(uint64(input), 10)
}
func FindCfRayHeader(req *http.Request) string {
return req.Header.Get("Cf-Ray")
}
func IsLBProbeRequest(req *http.Request) bool {
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
}
+23 -7
View File
@@ -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 {
+4 -3
View File
@@ -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
}
@@ -222,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
View File
@@ -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")
}
+3 -1
View File
@@ -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
}
+10 -3
View File
@@ -58,7 +58,7 @@ func (o *Observer) logTrialHostname(registration *tunnelpogs.TunnelRegistration)
// 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) {
for _, line := range AsciiBox(TrialZoneMsg(registrationURL.String()), 2) {
o.log.Info().Msg(line)
}
} else {
@@ -70,7 +70,7 @@ func (o *Observer) logTrialHostname(registration *tunnelpogs.TunnelRegistration)
}
// Print out the given lines in a nice ASCII box.
func asciiBox(lines []string, padding int) (box []string) {
func AsciiBox(lines []string, padding int) (box []string) {
maxLen := maxLen(lines)
spacer := strings.Repeat(" ", padding)
@@ -94,7 +94,7 @@ func maxLen(lines []string) int {
return max
}
func trialZoneMsg(url string) []string {
func TrialZoneMsg(url string) []string {
return []string{
"Your free tunnel has started! Visit it:",
" " + url,
@@ -111,6 +111,13 @@ func (o *Observer) sendConnectedEvent(connIndex uint8, location string) {
func (o *Observer) sendURL(url string) {
o.sendEvent(Event{EventType: SetURL, URL: url})
if !strings.HasPrefix(url, "https://") {
// We add https:// in the prefix for backwards compatibility as we used to do that with the old free tunnels
// and some tools (like `wrangler tail`) are regexp-ing for that specifically.
url = "https://" + url
}
o.metrics.userHostnamesCounts.WithLabelValues(url).Inc()
}
func (o *Observer) SendReconnect(connIndex uint8) {
+19
View File
@@ -6,9 +6,28 @@ import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
)
func TestSendUrl(t *testing.T) {
observer := NewObserver(&log, &log, false)
observer.sendURL("my-url.com")
assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://my-url.com"))
observer.sendURL("https://another-long-one.com")
assert.Equal(t, 1.0, getCounterValue(t, observer.metrics.userHostnamesCounts, "https://another-long-one.com"))
}
func getCounterValue(t *testing.T, metric *prometheus.CounterVec, val string) float64 {
var m = &dto.Metric{}
err := metric.WithLabelValues(val).Write(m)
assert.NoError(t, err)
return m.Counter.GetValue()
}
func TestRegisterServerLocation(t *testing.T) {
m := newTunnelMetrics()
tunnels := 20
+4 -1
View File
@@ -172,7 +172,10 @@ func NewProtocolSelector(
http2Percentage, err := fetchFunc()
if err != nil {
return nil, err
log.Err(err).Msg("Unable to lookup protocol. Defaulting to `http2`. If this fails, you can set `--protocol h2mux` in your cloudflared command.")
return &staticProtocolSelector{
current: HTTP2,
}, nil
}
if protocolFlag == HTTP2.String() {
if http2Percentage < 0 {
+2 -1
View File
@@ -158,7 +158,8 @@ func TestNewProtocolSelector(t *testing.T) {
protocol: "unknown",
fetchFunc: mockFetcherWithError(),
namedTunnelConfig: testNamedTunnelConfig,
wantErr: true,
expectedProtocol: HTTP2,
wantErr: false,
},
}
+45 -32
View File
@@ -10,6 +10,9 @@ import shutil
import hashlib
import requests
import tarfile
from os import listdir
from os.path import isfile, join
import re
from github import Github, GithubException, UnknownObjectException
@@ -135,7 +138,7 @@ def parse_args():
logging.error("Missing asset path")
is_valid = False
if not args.name:
if not args.name and not os.path.isdir(args.path):
logging.error("Missing asset name")
is_valid = False
@@ -161,6 +164,38 @@ def parse_args():
parser.print_usage()
exit(1)
def upload_asset(release, filepath, filename, release_version, kv_account_id, namespace_id, kv_api_token):
logging.info("Uploading asset: %s", filename)
release.upload_asset(filepath, name=filename)
# check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
binary_path = filepath
if binary_path.endswith("tgz"):
try:
shutil.rmtree('cfd')
except OSError:
pass
zipfile = tarfile.open(binary_path, "r:gz")
zipfile.extractall('cfd') # specify which folder to extract to
zipfile.close()
binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared')
# send the sha256 (the checksum) to workers kv
pkg_hash = get_sha256(binary_path)
send_hash(pkg_hash, filename, release_version, kv_account_id, namespace_id, kv_api_token)
# create the artifacts directory if it doesn't exist
artifact_path = os.path.join(os.getcwd(), 'artifacts')
if not os.path.isdir(artifact_path):
os.mkdir(artifact_path)
# copy the binary to the path
copy_path = os.path.join(artifact_path, filename)
try:
shutil.copy(filepath, copy_path)
except shutil.SameFileError:
pass # the macOS release copy fails with being the same file (already in the artifacts directory)
def main():
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
@@ -174,40 +209,18 @@ def main():
logging.info("Skipping asset upload because of dry-run")
return
release.upload_asset(args.path, name=args.name)
# check and extract if the file is a tar and gzipped file (as is the case with the macos builds)
binary_path = args.path
if binary_path.endswith("tgz"):
try:
shutil.rmtree('cfd')
except OSError as e:
pass
zipfile = tarfile.open(binary_path, "r:gz")
zipfile.extractall('cfd') # specify which folder to extract to
zipfile.close()
binary_path = os.path.join(os.getcwd(), 'cfd', 'cloudflared')
# send the sha256 (the checksum) to workers kv
pkg_hash = get_sha256(binary_path)
send_hash(pkg_hash, args.name, args.release_version, args.kv_account_id, args.namespace_id, args.kv_api_token)
# create the artifacts directory if it doesn't exist
artifact_path = os.path.join(os.getcwd(), 'artifacts')
if not os.path.isdir(artifact_path):
os.mkdir(artifact_path)
# copy the binary to the path
copy_path = os.path.join(artifact_path, args.name)
try:
shutil.copy(args.path, copy_path)
except shutil.SameFileError:
pass # the macOS release copy fails with being the same file (already in the artifacts directory). Catching to ignore.
if os.path.isdir(args.path):
onlyfiles = [f for f in listdir(args.path) if isfile(join(args.path, f))]
for filename in onlyfiles:
binary_path = os.path.join(args.path, filename)
upload_asset(release, binary_path, filename, args.release_version, args.kv_account_id, args.namespace_id,
args.kv_api_token)
else:
upload_asset(release, args.path, args.name, args.release_version, args.kv_account_id, args.namespace_id,
args.kv_api_token)
except Exception as e:
logging.exception(e)
exit(1)
main()
+2 -2
View File
@@ -103,7 +103,7 @@ func NewWarpRoutingService() *WarpRoutingService {
}
// Get a single origin service from the CLI/config.
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (originService, error) {
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginService, error) {
if c.IsSet("hello-world") {
return new(helloWorld), nil
}
@@ -167,7 +167,7 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
rules := make([]Rule, len(ingress))
for i, r := range ingress {
cfg := setConfig(defaults, r.OriginRequest)
var service originService
var service OriginService
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
// No validation necessary for unix socket filepath services
-66
View File
@@ -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
View File
@@ -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
}
+5 -86
View File
@@ -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{}) {
+3 -3
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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,
+140 -88
View File
@@ -7,24 +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 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
@@ -32,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,
@@ -47,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{
@@ -85,26 +72,69 @@ 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 {
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 {
rule, srv := ruleField(p.ingressRules, ruleNum)
p.logRequestError(err, cfRay, rule, srv)
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
}
@@ -116,26 +146,37 @@ func ruleField(ing ingress.Ingress, ruleNum int) (ruleID string, srv string) {
return fmt.Sprintf("%d", ruleNum), srv
}
func (p *proxy) proxyHTTPRequest(w connection.ResponseWriter, req *http.Request, rule *ingress.Rule, fields logFields) error {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if rule.Config.DisableChunkedEncoding {
req.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
req.ContentLength = int64(cLength)
// ProxyHTTPRequest proxies requests of underlying type http and websocket to the origin service.
func (p *Proxy) proxyHTTPRequest(
w connection.ResponseWriter,
req *http.Request,
httpService ingress.HTTPOriginProxy,
isWebsocket bool,
disableChunkedEncoding bool,
fields logFields,
) error {
roundTripReq := req
if isWebsocket {
roundTripReq = req.Clone(req.Context())
roundTripReq.Header.Set("Connection", "Upgrade")
roundTripReq.Header.Set("Upgrade", "websocket")
roundTripReq.Header.Set("Sec-Websocket-Version", "13")
roundTripReq.ContentLength = 0
roundTripReq.Body = nil
} else {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if disableChunkedEncoding {
roundTripReq.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
roundTripReq.ContentLength = int64(cLength)
}
}
// Request origin to keep connection alive to improve performance
roundTripReq.Header.Set("Connection", "keep-alive")
}
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
httpService, ok := rule.Service.(ingress.HTTPOriginProxy)
if !ok {
p.log.Error().Msgf("%s is not a http service", rule.Service)
return fmt.Errorf("Not a http service")
}
resp, err := httpService.RoundTrip(req)
resp, err := httpService.RoundTrip(roundTripReq)
if err != nil {
return errors.Wrap(err, "Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared")
}
@@ -145,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)
@@ -155,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() {
@@ -189,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
}
@@ -211,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')
@@ -222,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)
}
@@ -234,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 {
@@ -257,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)
@@ -275,7 +326,7 @@ func (p *proxy) logOriginResponse(resp *http.Response, fields logFields) {
}
}
func (p *proxy) logRequestError(err error, cfRay string, rule, service string) {
func (p *Proxy) logRequestError(err error, cfRay string, rule, service string) {
requestErrors.Inc()
log := p.log.Error().Err(err)
if cfRay != "" {
@@ -290,10 +341,11 @@ func (p *proxy) logRequestError(err error, cfRay string, rule, service string) {
log.Msg("")
}
func findCfRayHeader(req *http.Request) string {
return req.Header.Get("Cf-Ray")
}
func isLBProbeRequest(req *http.Request) bool {
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
func getDestFromRule(rule *ingress.Rule, req *http.Request) (string, error) {
switch rule.Service.String() {
case ingress.ServiceBastion:
return carrier.ResolveBastionDest(req)
default:
return rule.Service.String(), nil
}
}
+99 -31
View File
@@ -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 -1
View File
@@ -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"
)
@@ -417,6 +416,7 @@ func ServeHTTP2(
config.Observer,
connIndex,
connectedFuse,
config.Log,
gracefulShutdownC,
)
+88
View File
@@ -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
}
+115
View File
@@ -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
}
+88
View File
@@ -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))
}
})
}
}
+28
View File
@@ -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);
}
+395
View File
@@ -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 -1
View File
@@ -278,7 +278,7 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
// 403/401 from the edge will have aud in a header
aud = audHeader
} else {
return nil, fmt.Errorf("failed to get Access app info for %s", reqURL.String())
return nil, fmt.Errorf("failed to find Access application at %s", reqURL.String())
}
domain := resp.Header.Get(appDomainHeader)
+11 -1
View File
@@ -88,6 +88,7 @@ type DNSRoute struct {
type DNSRouteResult struct {
route *DNSRoute
CName Change `json:"cname"`
Name string `json:"name"`
}
func NewDNSRoute(userHostname string, overwriteExisting bool) Route {
@@ -135,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 {
+8
View File
@@ -25,6 +25,14 @@ func (f *Filter) ByName(name string) {
f.queryParams.Set("name", name)
}
func (f *Filter) ByNamePrefix(namePrefix string) {
f.queryParams.Set("name_prefix", namePrefix)
}
func (f *Filter) ExcludeNameWithPrefix(excludePrefix string) {
f.queryParams.Set("exclude_prefix", excludePrefix)
}
func (f *Filter) NoDeleted() {
f.queryParams.Set("is_deleted", "false")
}