Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f109919
Add dependencies for QR code generation
elalemanyo Aug 15, 2026
ea64b5e
Implement QR code generation and display for quick tunnels
elalemanyo Aug 15, 2026
d7ac7ef
chore: Update distrolesss images in amd64
jcsf Aug 20, 2026
4960a5c
chore: bump gorilla/websocket for GO-2026-6278
DevinCarr Aug 26, 2026
a030944
TUN-10725: Remove unused certificate configuration
DevinCarr Aug 26, 2026
7c4688a
VULN-142146: Update GitLab R2 token path (cloudflared-pkgs) to protec…
jcsf Aug 26, 2026
f6debd2
TUN-10820: Remove test-only stdin reconnect control
DevinCarr Aug 26, 2026
b5f750e
chore: Clean up unused code
DevinCarr Aug 27, 2026
0f9546b
TUN-10822: Remove fetching protocol percentage from remote
DevinCarr Aug 27, 2026
1d91448
chore: Remove stale metrics timeout TODO
Aug 28, 2026
0f4b3ab
TUN-10798: Parse Quick Tunnel allowed mail rules
Aug 28, 2026
47e9820
Release 2026.8.3
jcsf Aug 31, 2026
06da2a0
fix(ci): use fresh clone for Windows jobs
Sep 1, 2026
0b15256
AUTH-8883 Fix Access token lock self-deadlock during same-process reauth
jroyal Sep 2, 2026
323b4dc
TUN-10829: Remove vendoring
DevinCarr Sep 2, 2026
9a2a271
TUN-10799: Negotiate protected mode during Quick Tunnel provisioning
Sep 3, 2026
a07f833
TUN-10834: Bump Go toolchains to 1.26.8
DevinCarr Sep 3, 2026
89d3c7d
Update gcr.io/distroless/base-debian13:nonroot-amd64 Docker digest to…
Sep 8, 2026
2c04c51
Update gcr.io/distroless/base-debian13:nonroot Docker digest to d199d20
Sep 8, 2026
32651c9
TUN-10738: Propagate edge registration errors over QUIC and fix super…
joliveirinha Sep 8, 2026
a7c379c
chore: Cleanup unused transport log level
DevinCarr Sep 9, 2026
d9cb048
Release 2026.9.0
DevinCarr Sep 9, 2026
ef3be07
Revert "chore: Cleanup unused transport log level"
macmarcelino Sep 10, 2026
2e6b544
chore: Add notes on breaking changes
macmarcelino Sep 10, 2026
fbf8186
chore: Mark transport-loglevel deprecated
DevinCarr Sep 10, 2026
6d9d76d
Release 2026.9.1
DevinCarr Sep 10, 2026
eb5f263
ci: Disable macOS builds while runners are unavailable
jcsf Sep 14, 2026
6dec8fe
TUN-10800: Add browser-bound login state
Sep 15, 2026
faec4a8
TUN-10800: Consume authentication callback state
Sep 15, 2026
f80dc04
Merge branch 'master' into feature/quick-tunnel-qr-code
elalemanyo Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 85 additions & 8 deletions cmd/cloudflared/tunnel/quick_tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/google/uuid"
"github.com/pkg/errors"
"rsc.io/qr"

"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
Expand All @@ -19,6 +20,10 @@ import (

const httpTimeout = 15 * time.Second

// qrQuietZoneModules is the number of empty modules added around the rendered
// QR code. Four modules is the minimum quiet zone required by the QR spec.
const qrQuietZoneModules = 4

const disclaimer = "Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee, are subject to the Cloudflare Online Services Terms of Use (https://www.cloudflare.com/website-terms/), and Cloudflare reserves the right to investigate your use of Tunnels for violations of such terms. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps"

const (
Expand Down Expand Up @@ -116,15 +121,21 @@ func RunQuickTunnel(sc *subcommandContext) error {
TunnelID: tunnelID,
}

url := data.Result.Hostname
if !strings.HasPrefix(url, "https://") {
url = "https://" + url
}
cliutil.LogTable(sc.log, quickTunnelURLDisplayLines(data.Result.Hostname))

cliutil.LogTable(sc.log, []string{
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
url,
})
quickTunnelQRLines, err := quickTunnelQRCodeLines(data.Result.Hostname)
if err != nil {
sc.log.Warn().Err(err).Msg("Failed to generate quick Tunnel QR code")
} else {
// Filter out the all-white quiet-zone rows so the terminal output
// stays compact while the QR code itself remains scannable.
for _, line := range quickTunnelQRLines {
if line != "" {
sc.log.Info().Msg(line)
}
}
sc.log.Info().Msg("")
}

if !sc.c.IsSet(flags.Protocol) {
_ = sc.c.Set(flags.Protocol, "quic")
Expand All @@ -141,6 +152,72 @@ func RunQuickTunnel(sc *subcommandContext) error {
)
}

func quickTunnelURLDisplayLines(hostname string) []string {
return []string{
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
normalizeQuickTunnelURL(hostname),
}
}

func quickTunnelQRCodeLines(hostname string) ([]string, error) {
url := normalizeQuickTunnelURL(hostname)
code, err := qr.Encode(url, qr.L)
if err != nil {
return nil, errors.Wrap(err, "failed to create quick Tunnel QR code")
}

return renderHalfBlockQRCode(code, qrQuietZoneModules), nil
}

func renderHalfBlockQRCode(code *qr.Code, quietZone int) []string {
minX, minY, maxX, maxY := code.Size, code.Size, 0, 0
for y := 0; y < code.Size; y++ {
for x := 0; x < code.Size; x++ {
if code.Black(x, y) {
minX = min(minX, x)
minY = min(minY, y)
maxX = max(maxX, x)
maxY = max(maxY, y)
}
}
}

minX -= quietZone
minY -= quietZone
maxX += quietZone
maxY += quietZone

lines := make([]string, 0, ((maxY-minY)+2)/2)
lineWidth := maxX - minX + 1
for y := minY; y <= maxY; y += 2 {
var line strings.Builder
line.Grow(lineWidth)
for x := minX; x <= maxX; x++ {
top := code.Black(x, y)
bottom := y+1 <= maxY && code.Black(x, y+1)
switch {
case top && bottom:
line.WriteRune('█')
case top:
line.WriteRune('▀')
case bottom:
line.WriteRune('▄')
default:
line.WriteRune(' ')
}
}
lines = append(lines, line.String())
}
return lines
}

func normalizeQuickTunnelURL(hostname string) string {
if strings.HasPrefix(hostname, "https://") {
return hostname
}
return "https://" + hostname
}

type QuickTunnelResponse struct {
Success bool
Result QuickTunnel
Expand Down
103 changes: 103 additions & 0 deletions cmd/cloudflared/tunnel/quick_tunnel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,115 @@ package tunnel

import (
"encoding/json"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"rsc.io/qr"
)

func TestQuickTunnelURLDisplayLinesNormalizeURL(t *testing.T) {
t.Parallel()

lines := quickTunnelURLDisplayLines("example.trycloudflare.com")

require.Len(t, lines, 2)
assert.Equal(t, "Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):", lines[0])
assert.Equal(t, "https://example.trycloudflare.com", lines[1])
}

func TestQuickTunnelURLDisplayLinesPreserveHTTPSURL(t *testing.T) {
t.Parallel()

lines := quickTunnelURLDisplayLines("https://example.trycloudflare.com")

require.Len(t, lines, 2)
assert.Equal(t, "https://example.trycloudflare.com", lines[1])
}

func TestQuickTunnelQRCodeLinesUseCompactTerminalBlocks(t *testing.T) {
t.Parallel()

lines, err := quickTunnelQRCodeLines("example.trycloudflare.com")

require.NoError(t, err)
require.NotEmpty(t, lines)
qrOutput := strings.Join(lines, "\n")
assert.Contains(t, qrOutput, "▀")
assert.Contains(t, qrOutput, "▄")
assert.NotContains(t, qrOutput, "https://example.trycloudflare.com")
}

func TestQuickTunnelQRCodeLinesKeepScanQuietZone(t *testing.T) {
t.Parallel()

lines, err := quickTunnelQRCodeLines("example.trycloudflare.com")

require.NoError(t, err)
require.Greater(t, len(lines), 4)
assert.Empty(t, strings.TrimSpace(lines[0]))
assert.Empty(t, strings.TrimSpace(lines[1]))
assert.Empty(t, strings.TrimSpace(lines[len(lines)-2]))
assert.Empty(t, strings.TrimSpace(lines[len(lines)-1]))
assert.NotEmpty(t, strings.TrimSpace(lines[2]))
for _, line := range lines[2 : len(lines)-2] {
assert.True(t, strings.HasPrefix(line, " "))
}
}

func TestQuickTunnelQRCodeLinesReturnsErrorForURLTooLong(t *testing.T) {
t.Parallel()

// A URL longer than the largest QR version can encode.
longURL := strings.Repeat("a", 10000)

_, err := quickTunnelQRCodeLines(longURL)

require.Error(t, err)
assert.Contains(t, err.Error(), "failed to create quick Tunnel QR code")
}

func TestRenderHalfBlockQRCodeMatchesSourceBitmap(t *testing.T) {
t.Parallel()

url := "https://example.trycloudflare.com"
code, err := qr.Encode(url, qr.L)
require.NoError(t, err)

quietZone := 2
lines := renderHalfBlockQRCode(code, quietZone)
require.NotEmpty(t, lines)

// Reconstruct a per-module bitmap from the half-block terminal output
// and compare it to the original QR code.
for row, line := range lines {
yTop := row*2 - quietZone
yBottom := yTop + 1
col := 0
for _, r := range line {
x := col - quietZone
switch r {
case '█':
assert.True(t, code.Black(x, yTop), "expected black at (%d,%d)", x, yTop)
assert.True(t, code.Black(x, yBottom), "expected black at (%d,%d)", x, yBottom)
case '▀':
assert.True(t, code.Black(x, yTop), "expected black at (%d,%d)", x, yTop)
assert.False(t, code.Black(x, yBottom), "expected white at (%d,%d)", x, yBottom)
case '▄':
assert.False(t, code.Black(x, yTop), "expected white at (%d,%d)", x, yTop)
assert.True(t, code.Black(x, yBottom), "expected black at (%d,%d)", x, yBottom)
case ' ':
assert.False(t, code.Black(x, yTop), "expected white at (%d,%d)", x, yTop)
assert.False(t, code.Black(x, yBottom), "expected white at (%d,%d)", x, yBottom)
default:
t.Fatalf("unexpected rune %q at row %d col %d", r, row, col)
}
col++
}
}
}

func TestBuildQuickTunnelRequestBody_PublicMode(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
nhooyr.io/websocket v1.8.7
rsc.io/qr v0.2.0
zombiezen.com/go/capnproto2 v2.18.0+incompatible
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -298,5 +298,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
zombiezen.com/go/capnproto2 v2.18.0+incompatible h1:mwfXZniffG5mXokQGHUJWGnqIBggoPfT/CEwon9Yess=
zombiezen.com/go/capnproto2 v2.18.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ=