Compare commits

...

10 Commits

Author SHA1 Message Date
João "Pisco" Fernandes 8679787525 Release 2026.7.2 2026-07-15 12:01:07 +01:00
Rhys Rustad-Elliott 45c0f22e97 VULN-118896: MacOS service: use --token-file instead of --token
* Refactor directory-related functions in macos_service.go to pull out common logic
* Use --token-file instead of --token when installing cloudflared with a token on MacOS such that the secret token will not show up in the output of "ps aux" (or similar)
* Improve the "service install" help text on MacOS to distinguish between the "service install " and "service install \<token\>" cases
2026-07-15 10:52:25 +01:00
João "Pisco" Fernandes e8956c10ac Update gcr.io/distroless/base-debian13:nonroot-arm Docker digest to 7a4876f88 2026-07-14 19:31:56 +01:00
Renovate 541a608dde Update gcr.io/distroless/base-debian13:nonroot-amd64 Docker digest to ce2a20e 2026-07-14 18:36:36 +01:00
Miguel da Costa Martins Marcelino 6fd720a4a4 chore: Change internal image name
Changing image name. The image is currently under `/tun/cloudflared/cloudflared-daemon/master`. We want to instead have it under `/tun/cloudflared/cloudflared/master`. Adding the `daemon` suffix does not add much value.
2026-07-14 16:43:53 +00:00
Renovate 60a8c3da17 Update gcr.io/distroless/base-debian13:nonroot Docker digest to b78832f
This MR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| gcr.io/distroless/base-debian13 | final | digest | `ab7554b` → `b78832f` |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At 12:00 AM through 04:59 AM and 10:00 PM through 11:59 PM, Monday through Friday (`* 0-4,22-23 * * 1-5`)
  - Only on Sunday and Saturday (`* * * * 0,6`)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTcuNSIsInVwZGF0ZWRJblZlciI6IjQzLjI1Ny41IiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->
2026-07-14 17:40:32 +01:00
João "Pisco" Fernandes 2601f87b57 chore: Remove bookworm internal register publish
Bookworm has been deprecated and therefore our internal
repository no longer accepts the publish of new packages.
We are then removing the publish to the internal repositories of bookworm.
2026-07-13 11:28:14 +01:00
Rhys Rustad-Elliott f70adda11c VULN-118896: Linux service: Use --token-file instead of --token
When installed with a token, e.g.,

$ cloudflared service install \<token\>

cloudflared will set itself up to be run by the init system using the --token argument. This results in tokens being visible in the output of ps aux by unprivileged users. Change this such that this installation method instead puts the token in a file with mode 600 in /etc/cloudflared and uses the --token-file flag instead.
2026-07-13 09:57:07 +00:00
João "Pisco" Fernandes ecb88678f1 Release 2026.7.1 2026-07-09 13:06:21 +01:00
Miguel da Costa Martins Marcelino 43bfec0bcd Revert "TUN-10621: Propagate max wait timeout"
This reverts merge request !1859
2026-07-09 13:00:02 +01:00
20 changed files with 259 additions and 334 deletions
+8 -37
View File
@@ -3,23 +3,12 @@
runOnBranches: "^master$"
COMPONENT: "common"
.register_inputs_stable_bookworm: &register_inputs_stable_bookworm
<<: *register_inputs
runOnChangesTo: ['RELEASE_NOTES']
FLAVOR: "bookworm"
SERIES: "stable"
.register_inputs_stable_trixie: &register_inputs_stable_trixie
<<: *register_inputs
runOnChangesTo: ['RELEASE_NOTES']
FLAVOR: "trixie"
SERIES: "stable"
.register_inputs_next_bookworm: &register_inputs_next_bookworm
<<: *register_inputs
FLAVOR: "bookworm"
SERIES: next
.register_inputs_next_trixie: &register_inputs_next_trixie
<<: *register_inputs
FLAVOR: "trixie"
@@ -66,6 +55,10 @@ cloudflared-arm64-stable:
ORIGINAL_NAME: true
CGO_ENABLED: 1
# Jobs names
.amd64-stable: &amd64-stable ["cloudflared-amd64-stable"]
.arm64-stable: &arm64-stable ["cloudflared-arm64-stable"]
############
### Next ###
############
@@ -85,25 +78,16 @@ cloudflared-arm64-next:
<<: *arm64-stable-vars
NIGHTLY: true
# Jobs names
.amd64-next: &amd64-next ["cloudflared-amd64-next"]
.arm64-next: &arm64-next ["cloudflared-arm64-next"]
include:
- local: .ci/commons.gitlab-ci.yml
##########################################
### Publish Packages to Internal Repos ###
##########################################
# Bookworm AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_bookworm
jobPrefix: cloudflared-bookworm-amd64
needs: &amd64-stable ["cloudflared-amd64-stable"]
# Bookworm ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_stable_bookworm
jobPrefix: cloudflared-bookworm-arm64
needs: &arm64-stable ["cloudflared-arm64-stable"]
# Trixie AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
@@ -122,19 +106,6 @@ include:
##################################################
### Publish Nightly Packages to Internal Repos ###
##################################################
# Bookworm AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_bookworm
jobPrefix: cloudflared-nightly-bookworm-amd64
needs: &amd64-next ['cloudflared-amd64-next']
# Bookworm ARM64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
inputs:
<<: *register_inputs_next_bookworm
jobPrefix: cloudflared-nightly-bookworm-arm64
needs: &arm64-next ['cloudflared-arm64-next']
# Trixie AMD64
- component: $CI_SERVER_FQDN/cloudflare/ci/apt-register/register@~latest
+1 -1
View File
@@ -1,5 +1,5 @@
images:
- name: cloudflared-daemon
- name: cloudflared
dockerfile: Dockerfile.$ARCH
context: .
version_file: versions-internal
+1 -1
View File
@@ -20,7 +20,7 @@ COPY . .
RUN make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian13:nonroot@sha256:ab7554b6d07ad354fad31957f8a1a813e65dfb93a8ad160568c79c3f2be6884f
FROM gcr.io/distroless/base-debian13:nonroot@sha256:b78832f41c8128046807c24840ebee4f1c18ba7870eed423d8750c272c15e147
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+1 -1
View File
@@ -15,7 +15,7 @@ COPY . .
RUN GOOS=linux GOARCH=amd64 make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian13:nonroot-amd64@sha256:ced0a2b1936b14d5bddc2ee02a807b1586ca6576a967f5b043f4a3301c8a8f6b
FROM gcr.io/distroless/base-debian13:nonroot-amd64@sha256:ce2a20e0e277b7d913aa8bcfa098fc2a543dc08028f7393434963fa24b39ea81
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+1 -1
View File
@@ -15,7 +15,7 @@ COPY . .
RUN GOOS=linux GOARCH=arm64 make cloudflared
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian13:nonroot-arm64@sha256:9c1ab6a3dbf9e22827b0be4a314d7cfbe008f922b7ca833ed0e5a63318c6169e
FROM gcr.io/distroless/base-debian13:nonroot-arm64@sha256:7a4876f88e7fe3190972c274b679b3473f61e8d990a4dce3627961b3e22a0eaf
LABEL org.opencontainers.image.source="https://github.com/cloudflare/cloudflared"
+12
View File
@@ -1,3 +1,15 @@
2026.7.2
- 2026-07-15 VULN-118896: MacOS service: use --token-file instead of --token
- 2026-07-14 Update gcr.io/distroless/base-debian13:nonroot Docker digest to b78832f
- 2026-07-14 chore: Change internal image name
- 2026-07-14 Update gcr.io/distroless/base-debian13:nonroot-arm Docker digest to 7a4876f88
- 2026-07-13 VULN-118896: Linux service: Use --token-file instead of --token
- 2026-07-13 chore: Remove bookworm internal register publish
- 2026-07-13 Update gcr.io/distroless/base-debian13:nonroot-amd64 Docker digest to ce2a20e
2026.7.1
- 2026-07-09 Revert "TUN-10621: Propagate max wait timeout"
2026.7.0
- 2026-07-08 chore: Bump go-chi to version 5.3.1
- 2026-07-08 Revert "TUN-10557: Bump quic-go v0.59.1"
+1 -1
View File
@@ -37,7 +37,7 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
}
defer func() { _ = wsConn.Close() }()
stream.Pipe(wsConn, conn, stream.DefaultTimeoutAfterFirstClose, ws.log)
stream.Pipe(wsConn, conn, ws.log)
return nil
}
-139
View File
@@ -5,12 +5,8 @@ import (
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"time"
@@ -21,7 +17,6 @@ import (
"golang.org/x/net/websocket"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/stream"
"github.com/cloudflare/cloudflared/tlsconfig"
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
)
@@ -132,137 +127,3 @@ func TestWebsocketWrapper(t *testing.T) {
require.Equal(t, 2, n)
require.Equal(t, "bc", string(buf[:n]))
}
// halfClosePipeEnd is a bidirectional pipe built from two independent io.Pipe
// pairs. Unlike net.Pipe, closing one direction does not affect the other, which
// lets us simulate a half-close: the local client can signal EOF on its write
// side while still reading a delayed response from the remote side.
type halfClosePipeEnd struct {
r *io.PipeReader // data flowing into this end
w *io.PipeWriter // data flowing out of this end
}
func newHalfClosePipe() (local, remote *halfClosePipeEnd) {
// Pipe A carries data from local → remote.
ar, aw := io.Pipe()
// Pipe B carries data from remote → local.
br, bw := io.Pipe()
local = &halfClosePipeEnd{r: br, w: aw}
remote = &halfClosePipeEnd{r: ar, w: bw}
return local, remote
}
func (p *halfClosePipeEnd) Read(b []byte) (int, error) { return p.r.Read(b) }
func (p *halfClosePipeEnd) Write(b []byte) (int, error) { return p.w.Write(b) }
// CloseWrite signals EOF to the remote reader without closing the read side.
func (p *halfClosePipeEnd) CloseWrite() error { return p.w.Close() }
// Close shuts down both directions.
func (p *halfClosePipeEnd) Close() error {
_ = p.r.Close()
return p.w.Close()
}
// TestServeStreamWaitsForResponseAfterLocalClose exercises the websocket path
// It verifies that the pipe does not tear down immediately after the client closes
// its write side.
//
// The setup mirrors the cloudflared access tcp path:
//
// local app -> halfClosePipe -> ServeStream -> mock WS echo server
//
// Sequence:
// 1. Write payload to the mock server through ServeStream.
// 2. Half-close the write side (CloseWrite) — signals EOF upstream.
// 3. Sleep for halfCloseWait to make the race window explicit: a buggy
// (timeout=0) pipe would already have torn down the connection here.
// 4. Read the echo — must succeed because the patch keeps the pipe alive.
func TestServeStreamWaitsForResponseAfterLocalClose(t *testing.T) {
t.Parallel()
const (
payload = "half-close-test"
// halfCloseWait makes the race window visible: if ServeStream tears
// down the connection on CloseWrite the read that follows will fail
// immediately, mirroring the 3-second sleep in the cftunnel reference
// test. It must be shorter than DefaultTimeoutAfterFirstClose (10s).
halfCloseWait = 3 * time.Second
// testTimeout is an upper bound for the whole test.
testTimeout = stream.DefaultTimeoutAfterFirstClose + 5*time.Second
)
server := websocketServer()
defer server.Close()
localEnd, remoteEnd := newHalfClosePipe()
log := zerolog.Nop()
wsConn := NewWSConnection(&log)
options := &StartOptions{
OriginURL: "ws://" + server.Listener.Addr().String(),
}
serveErrCh := make(chan error, 1)
go func() {
serveErrCh <- wsConn.ServeStream(options, remoteEnd)
}()
ctx, cancel := context.WithTimeout(t.Context(), testTimeout)
defer cancel()
// 1. Write the payload. ServeStream forwards it as a WS binary frame.
_, err := localEnd.Write([]byte(payload))
require.NoError(t, err)
// 2. Half-close the write side
require.NoError(t, localEnd.CloseWrite())
// 3. Wait to make the race window explicit.
time.Sleep(halfCloseWait)
// 4. Read the echo.
got := make([]byte, len(payload))
_, err = io.ReadFull(localEnd, got)
require.NoError(t, err, "read after half-close failed: pipe was torn down too early")
require.Equal(t, payload, string(got))
// Drain ServeStream.
_ = localEnd.Close()
_ = remoteEnd.Close()
select {
case err := <-serveErrCh:
if err != nil && err != io.EOF && !errors.Is(err, io.ErrClosedPipe) {
require.NoError(t, err)
}
case <-ctx.Done():
t.Fatal("ServeStream did not return in time")
}
}
func websocketServer() *httptest.Server {
upgrader := gws.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(*http.Request) bool { return true },
}
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer func() { _ = conn.Close() }()
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
if err := conn.WriteMessage(gws.BinaryMessage, msg); err != nil {
return
}
}))
}
+71
View File
@@ -1,6 +1,11 @@
package main
import (
"errors"
"fmt"
"os"
"path"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
@@ -8,6 +13,71 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
)
const (
defaultTokenFile = "token"
tokenPerms os.FileMode = 0o600
)
func ensureConfigDirExists(configDir string) error {
if err := os.Mkdir(configDir, 0o755); err != nil { //nolint:gosec // config dir must be traversable by non-root user
if errors.Is(err, os.ErrExist) {
return nil
}
return fmt.Errorf("failed to create config dir at %s: %w", configDir, err)
}
return nil
}
func writeTokenToFile(path string, token string) error {
if _, err := tunnel.ParseToken(token); err != nil {
return cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
}
if err := os.WriteFile(path, []byte(token), tokenPerms); err != nil {
return fmt.Errorf("failed to write token to %s: %w", path, err)
}
// If the token file already existed with unrestrictive perms, os.WriteFile
// above will not update them
if err := os.Chmod(path, tokenPerms); err != nil {
return fmt.Errorf("failed to restrict permissions on token file %s: %w", path, err)
}
return nil
}
func removeTokenFile(configDir string, log *zerolog.Logger) {
tp := tokenPath(configDir)
err := os.Remove(tp)
if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Warn().Msgf("Could not remove service token file at %s: %v", tp, err)
}
}
func buildArgsForTokenFile(configDir string) []string {
return []string{
"tunnel", "run", "--token-file", tokenPath(configDir),
}
}
func tokenPath(configDir string) string {
return path.Join(configDir, defaultTokenFile)
}
func writeTokenToConfigDir(c *cli.Context, configDir string) error {
if err := ensureConfigDirExists(configDir); err != nil {
return err
}
if err := writeTokenToFile(tokenPath(configDir), c.Args().First()); err != nil {
return err
}
return nil
}
// nolint:unused // This function is used by the Windows build, the unused warning when building for Linux and MacOS is spurious
func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
token := c.Args().First()
if _, err := tunnel.ParseToken(token); err != nil {
@@ -19,6 +89,7 @@ func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
}, nil
}
// nolint:unused // This function is used by the Windows build, the unused warning when building for Linux and MacOS is spurious
func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) {
if c.NArg() > 0 {
// currently, we only support extra args for token
+44 -20
View File
@@ -24,8 +24,21 @@ func runApp(app *cli.App, _ chan struct{}) {
Usage: "Manages the cloudflared system service",
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install cloudflared as a system service",
Name: "install",
Usage: "Install cloudflared as a system service",
ArgsUsage: "[TOKEN]",
Description: `
Installs cloudflared as a service using the detected init system (e.g., sysv,
systemd, openrc).
A token may optionally be provided. If a token is provided, it will be written
to disk in the service configuration directory and the cloudflared service
configured to use it via the --token-file argument.
If no token is provided, cloudflared will attempt to find a configuration file
with tunnel credentials from a predetermined list of configuration directory
paths. If found, it will use that configuration file and credentials (or error
out if no configuration file with credentials was found).`,
Action: cliutil.ConfiguredAction(installLinuxService),
Flags: []cli.Flag{
noUpdateServiceFlag,
@@ -246,23 +259,39 @@ func installLinuxService(c *cli.Context) error {
Path: etPath,
}
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
var extraArgs []string
if c.NArg() == 0 {
extraArgsFunc = buildArgsForConfig
// If passed no arguments e.g., "$ cloudflared service install",
// install the service using the detected config file (or error-out if
// no config exists).
if extraArgs, err = buildArgsForConfig(c, log); err != nil {
return err
}
} else {
extraArgsFunc = buildArgsForToken
}
// If passed one argument e.g., "$ cloudflared service install <token>"
// write the token to the config directory and install the service
// using --token-file pointing to that file. This is the quick setup
// the tunnel UI suggests.
extraArgs, err := extraArgsFunc(c, log)
if err != nil {
return err
// Ensure token file is removed if install fails
defer func() {
if err != nil {
removeTokenFile(serviceConfigDir, log)
}
}()
if err = writeTokenToConfigDir(c, serviceConfigDir); err != nil {
return fmt.Errorf("could not write token to configuration directory: %w", err)
}
extraArgs = buildArgsForTokenFile(serviceConfigDir)
}
templateArgs.ExtraArgs = extraArgs
// Check if the "no update flag" is set
autoUpdate := !c.IsSet(noUpdateServiceFlag.Name)
switch {
case inits.IsSystemd():
log.Info().Msgf("Using Systemd")
@@ -423,9 +452,12 @@ func uninstallLinuxService(c *cli.Context) error {
err = uninstallSysv(log)
}
removeTokenFile(serviceConfigDir, log)
if err == nil {
log.Info().Msg("Linux service for cloudflared uninstalled successfully")
}
return err
}
@@ -516,14 +548,6 @@ func uninstallOpenRC(log *zerolog.Logger) error {
return nil
}
func ensureConfigDirExists(configDir string) error {
ok, err := config.FileExists(configDir)
if !ok && err == nil {
err = os.Mkdir(configDir, 0o755) //nolint:gosec // config dir must be traversable by a non-root service user
}
return err
}
func copyFile(src, dest string) error {
srcFile, err := os.Open(src) //nolint:gosec // operator-provided service config path
if err != nil {
+80 -40
View File
@@ -5,6 +5,7 @@ package main
import (
"fmt"
"os"
"path"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
@@ -24,8 +25,19 @@ func runApp(app *cli.App, _ chan struct{}) {
Usage: "Manages the cloudflared launch agent",
Subcommands: []*cli.Command{
{
Name: "install",
Usage: "Install cloudflared as an user launch agent",
Name: "install",
Usage: "Install cloudflared as an user launch agent",
ArgsUsage: "[TOKEN]",
Description: `
Installs cloudflared as a launchd-managed service.
A token may optionally be provided. If a token is provided, it will be written
to disk in the service configuration directory and the cloudflared service
configured to use it via the --token-file argument.
If no token is provided, cloudflared will run without the --token-file argument,
causing it to look for credentials in a configuration file upon startup.`,
Action: cliutil.ConfiguredAction(installLaunchd),
},
{
@@ -76,38 +88,43 @@ func isRootUser() bool {
return os.Geteuid() == 0
}
func installPath() (string, error) {
// User is root, use /Library/LaunchDaemons instead of home directory
func resolveLibraryPath(subPath, fileName string) (string, error) {
// We use the system-wide /Library/... instead of ~/Library/... if the user is root
if isRootUser() {
return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", launchdIdentifier), nil
return path.Join("/Library", subPath, fileName), nil
}
userHomeDir, err := userHomeDir()
// This returns the home dir of the executing user using OS-specific method
// for discovering the home dir. It's not recommended to call this when the
// user has root permission as $HOME depends on what options the user uses
// with sudo.
userHomeDir, err := homedir.Dir()
if err != nil {
return "", err
return "", errors.Wrap(err, "Cannot determine home directory for the user")
}
return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", userHomeDir, launchdIdentifier), nil
return path.Join(userHomeDir, "Library", subPath, fileName), nil
}
// For docs on these subdirectories, see:
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html
func installPath() (string, error) {
subpath := "LaunchAgents"
if isRootUser() {
subpath = "LaunchDaemons"
}
return resolveLibraryPath(subpath, launchdIdentifier+".plist")
}
func stdoutPath() (string, error) {
if isRootUser() {
return fmt.Sprintf("/Library/Logs/%s.out.log", launchdIdentifier), nil
}
userHomeDir, err := userHomeDir()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/Library/Logs/%s.out.log", userHomeDir, launchdIdentifier), nil
return resolveLibraryPath("Logs", launchdIdentifier+".out.log")
}
func stderrPath() (string, error) {
if isRootUser() {
return fmt.Sprintf("/Library/Logs/%s.err.log", launchdIdentifier), nil
}
userHomeDir, err := userHomeDir()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/Library/Logs/%s.err.log", userHomeDir, launchdIdentifier), nil
return resolveLibraryPath("Logs", launchdIdentifier+".err.log")
}
func configPath() (string, error) {
return resolveLibraryPath("Application Support", launchdIdentifier)
}
func installLaunchd(c *cli.Context) error {
@@ -125,18 +142,45 @@ func installLaunchd(c *cli.Context) error {
etPath, err := os.Executable()
if err != nil {
log.Err(err).Msg("Error determining executable path")
return fmt.Errorf("Error determining executable path: %v", err)
return fmt.Errorf("Error determining executable path: %w", err)
}
installPath, err := installPath()
if err != nil {
log.Err(err).Msg("Error determining install path")
return errors.Wrap(err, "Error determining install path")
}
extraArgs, err := getServiceExtraArgsFromCliArgs(c, log)
if err != nil {
errMsg := "Unable to determine extra arguments for launch daemon"
log.Err(err).Msg(errMsg)
return errors.Wrap(err, errMsg)
var extraArgs []string
if c.NArg() > 0 {
// The service has been installed using a token e.g.,
// $ cloudflared service install <token>
//
// Write the token file to a config directory so we can start the
// daemon with --token-file
// Don't use :=, if we did so we would create a new err variable and
// shadow the outer one, causing the defer below to not have access to
// the outer err
var cp string
cp, err = configPath()
if err != nil {
log.Err(err).Msg("Error determining path to config directory")
return err
}
// Ensure token file is removed if install fails at any point from now
// on
defer func() {
if err != nil {
removeTokenFile(cp, log)
}
}()
if err = writeTokenToConfigDir(c, cp); err != nil {
return fmt.Errorf("could not write token to configuration directory: %w", err)
}
extraArgs = buildArgsForTokenFile(cp)
}
stdoutPath, err := stdoutPath()
@@ -206,17 +250,13 @@ func uninstallLaunchd(c *cli.Context) error {
if err == nil {
log.Info().Msg("Launchd for cloudflared was uninstalled successfully")
}
return err
}
func userHomeDir() (string, error) {
// This returns the home dir of the executing user using OS-specific method
// for discovering the home dir. It's not recommended to call this function
// when the user has root permission as $HOME depends on what options the user
// use with sudo.
homeDir, err := homedir.Dir()
cp, err := configPath()
if err != nil {
return "", errors.Wrap(err, "Cannot determine home directory for the user")
log.Err(err).Msg("error determining path to config directory, not removing token file")
return err
}
return homeDir, nil
removeTokenFile(cp, log)
return nil
}
+1 -1
View File
@@ -299,5 +299,5 @@ func wasInstalledFromPackageManager() bool {
}
func isRunningFromTerminal() bool {
return term.IsTerminal(int(os.Stdout.Fd())) //nolint:gosec
return term.IsTerminal(int(os.Stdout.Fd())) // nolint:gosec
}
+2 -2
View File
@@ -151,7 +151,7 @@ func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
}()
originConn := &echoPipe{reader: readPipe, writer: writePipe}
stream.Pipe(wsConn, originConn, 0, &log)
stream.Pipe(wsConn, originConn, &log)
cancel()
wsConn.Close()
return nil
@@ -190,7 +190,7 @@ func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
rInt, _ := rand.Int(rand.Reader, big.NewInt(50))
closedAfter := time.Millisecond * time.Duration(rInt.Int64())
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
stream.Pipe(wsConn, originConn, 0, &log)
stream.Pipe(wsConn, originConn, &log)
cancel()
wsConn.Close()
return nil
+3 -3
View File
@@ -25,9 +25,9 @@ type OriginConnection interface {
type streamHandlerFunc func(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger)
// DefaultStreamHandler is an implementation of streamHandlerFunc that
// performs a two-way io.Copy between originConn and remoteConn.
// performs a two way io.Copy between originConn and remoteConn.
func DefaultStreamHandler(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger) {
stream.Pipe(originConn, remoteConn, stream.DefaultTimeoutAfterFirstClose, log)
stream.Pipe(originConn, remoteConn, log)
}
// tcpConnection is an OriginConnection that directly streams to raw TCP.
@@ -38,7 +38,7 @@ type tcpConnection struct {
}
func (tc *tcpConnection) Stream(_ context.Context, tunnelConn io.ReadWriter, _ *zerolog.Logger) {
stream.Pipe(tunnelConn, tc, stream.DefaultTimeoutAfterFirstClose, tc.logger)
stream.Pipe(tunnelConn, tc, tc.logger)
}
func (tc *tcpConnection) Write(b []byte) (int, error) {
+1 -2
View File
@@ -38,7 +38,6 @@ func TestStreamTCPConnection(t *testing.T) {
tcpConn := tcpConnection{
Conn: cfdConn,
writeTimeout: 30 * time.Second,
logger: TestLogger,
}
eyeballConn, edgeConn := net.Pipe()
@@ -158,7 +157,7 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
require.NoError(t, err)
defer func() { _ = wsForwarderInConn.Close() }()
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, stream.DefaultTimeoutAfterFirstClose, TestLogger)
stream.Pipe(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, TestLogger)
return nil
})
+5 -5
View File
@@ -99,6 +99,7 @@ type rawTCPService struct {
name string
dialer net.Dialer
writeTimeout time.Duration
logger *zerolog.Logger
}
func (o *rawTCPService) String() string {
@@ -232,12 +233,10 @@ func (o *helloWorld) start(
if err != nil {
return errors.Wrap(err, "Cannot start Hello World Server")
}
go func() {
_ = hello.StartHelloWorldServer(log, helloListener, shutdownC)
}()
go hello.StartHelloWorldServer(log, helloListener, shutdownC)
o.server = helloListener
o.url = &url.URL{
o.httpService.url = &url.URL{
Scheme: "https",
Host: o.server.Addr().String(),
}
@@ -357,7 +356,7 @@ func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerol
IdleConnTimeout: cfg.KeepAliveTimeout.Duration,
TLSHandshakeTimeout: cfg.TLSTimeout.Duration,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify}, //nolint: gosec
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify},
ForceAttemptHTTP2: cfg.Http2Origin,
}
if _, isHelloWorld := service.(*helloWorld); !isHelloWorld && cfg.OriginServerName != "" {
@@ -375,6 +374,7 @@ func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerol
// DialContext depends on which kind of origin is being used.
dialContext := dialer.DialContext
switch service := service.(type) {
// If this origin is a unix socket, enforce network type "unix".
case *unixSocketPath:
httpTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
+3 -4
View File
@@ -256,7 +256,7 @@ func (p *Proxy) proxyHTTPRequest(
reader: tr.Body,
}
stream.Pipe(eyeballStream, rwc, time.Second*0, logger)
stream.Pipe(eyeballStream, rwc, logger)
return nil
}
@@ -311,7 +311,7 @@ func (p *Proxy) proxyStream(
// proxyTCPStream proxies private network type TCP connections as a stream towards an available origin.
//
// This is different from proxyStream because it's not leveraged ingress rule services and uses the
// This is different than proxyStream because it's not leveraged ingress rule services and uses the
// originDialer from OriginDialerService.
func (p *Proxy) proxyTCPStream(
tr *tracing.TracedContext,
@@ -344,8 +344,7 @@ func (p *Proxy) proxyTCPStream(
connectLatency.Observe(float64(time.Since(start).Milliseconds()))
logger.Debug().Msg("proxy stream acknowledged")
stream.Pipe(tunnelConn, originConn, stream.DefaultTimeoutAfterFirstClose, logger)
stream.Pipe(tunnelConn, originConn, logger)
return nil
}
+8 -10
View File
@@ -223,9 +223,9 @@ func testProxyWebsocket(proxy connection.OriginProxy) func(t *testing.T) {
}
if ctx.Err() == context.DeadlineExceeded {
t.Errorf("Test timed out")
_ = readPipe.Close()
_ = writePipe.Close()
_ = responseWriter.Close()
readPipe.Close()
writePipe.Close()
responseWriter.Close()
}
return nil
})
@@ -647,7 +647,7 @@ func TestConnections(t *testing.T) {
ingressServiceScheme: "tcp://",
originService: func(t *testing.T, ln net.Listener) {
// closing the listener created by the test.
_ = ln.Close()
ln.Close()
},
eyeballResponseWriter: newTCPRespWriter(replayer),
eyeballRequestBody: newTCPRequestBody([]byte("test2")),
@@ -756,8 +756,6 @@ func newTCPRequestBody(data []byte) *requestBody {
pr, pw := io.Pipe()
go func() {
_, _ = pw.Write(data)
// Close the write side once the payload has been sent.
_ = pw.Close()
}()
return &requestBody{
pr: pr,
@@ -803,8 +801,8 @@ func (p *pipedRequestBody) roundtrip(addr string) []byte {
if err != nil {
panic(err)
}
defer func() { _ = conn.Close() }()
defer func() { _ = resp.Body.Close() }()
defer conn.Close()
defer resp.Body.Close()
if resp.StatusCode != http.StatusSwitchingProtocols {
panic(fmt.Errorf("resp returned status code: %d", resp.StatusCode))
@@ -951,7 +949,7 @@ func runEchoTCPService(t *testing.T, l net.Listener) {
if err != nil {
panic(err)
}
defer func() { _ = conn.Close() }()
defer conn.Close()
for {
buf := make([]byte, 1024)
@@ -989,7 +987,7 @@ func runEchoWSService(t *testing.T, l net.Listener) {
t.Log(err)
return
}
defer func() { _ = conn.Close() }()
defer conn.Close()
for {
messageType, p, err := conn.ReadMessage()
+11 -21
View File
@@ -2,7 +2,6 @@ package stream
import (
"encoding/hex"
"errors"
"fmt"
"io"
"runtime/debug"
@@ -10,17 +9,12 @@ import (
"time"
"github.com/getsentry/sentry-go"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/cfio"
)
// DefaultTimeoutAfterFirstClose controls the upper bound of how long we wait for the second stream to finish.
// We use bidirectional streams for our communication. Since the read and write sides can be closed independently,
// we must have a way to close the second stream once the first one finishes. We don't want to wait indefinitely,
// since we want to prevent misbehaving clients from blocking cloudflared.
const DefaultTimeoutAfterFirstClose = time.Second * 10
type Stream interface {
Reader
WriterCloser
@@ -43,7 +37,7 @@ type nopCloseWriterAdapter struct {
io.ReadWriter
}
func noopCloseWriter(stream io.ReadWriter) *nopCloseWriterAdapter {
func NopCloseWriterAdapter(stream io.ReadWriter) *nopCloseWriterAdapter {
return &nopCloseWriterAdapter{stream}
}
@@ -78,7 +72,7 @@ func (s *bidirectionalStreamStatus) wait(maxWaitForSecondStream time.Duration) e
select {
case <-timer.C:
return fmt.Errorf("timeout waiting for second stream to finish %s", maxWaitForSecondStream)
return fmt.Errorf("timeout waiting for second stream to finish")
case <-s.doneChan:
return nil
}
@@ -91,19 +85,15 @@ func (s *bidirectionalStreamStatus) isAnyDone() bool {
}
// Pipe copies copy data to & from provided io.ReadWriters.
func Pipe(tunnelConn, originConn io.ReadWriter, timeoutAfterFirstClose time.Duration, log *zerolog.Logger) {
if err := PipeBidirectional(noopCloseWriter(tunnelConn), noopCloseWriter(originConn), timeoutAfterFirstClose, log); err != nil {
log.Warn().Err(err).Msg("Failed to pipe bidirectional stream")
}
func Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
_ = PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
}
// PipeBidirectional copies data between two unidirectional streams. It is a special case of Pipe that accepts streams
// whose read and write sides can be closed independently. The main difference is that when piping data from a reader
// to a writer, if EOF is read, this implementation propagates the EOF signal to the destination by closing the write
// side of the bidirectional stream.
// Finally, once EOF is received from one of the provided streams, the other direction has a configured grace period to
// finish; otherwise, the method returns a timeout error. It is, however, the responsibility of the caller to close
// the associated streams at both ends in order to free all resources and goroutines.
// PipeBidirectional copies data to two unidirectional streams. It is a special case of Pipe where it receives a concept that allows for Read and Write side to be closed independently.
// The main difference is that when piping data from a reader to a writer, if EOF is read, then this implementation propagates the EOF signal to the destination/writer by closing the write side of the
// Bidirectional Stream.
// Finally, depending on once EOF is ready from one of the provided streams, the other direction of streaming data will have a configured time period to also finish, otherwise,
// the method will return immediately with a timeout error. It is however, the responsibility of the caller to close the associated streams in both ends in order to free all the resources/go-routines.
func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error {
status := newBiStreamStatus()
@@ -111,7 +101,7 @@ func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.
go unidirectionalStream(upstream, downstream, "downstream->upstream", status, log)
if err := status.wait(maxWaitForSecondStream); err != nil {
return fmt.Errorf("unable to wait for both streams while proxying: %w", err)
return errors.Wrap(err, "unable to wait for both streams while proxying")
}
return nil
+5 -45
View File
@@ -30,8 +30,8 @@ func TestPipeBidirectionalFinishOneSideTimeout(t *testing.T) {
func TestPipeBidirectionalClosingWriteBothSidesAlsoExists(t *testing.T) {
fun := func(upstream, downstream *mockedStream) {
_ = downstream.CloseWrite()
_ = upstream.CloseWrite()
downstream.CloseWrite()
upstream.CloseWrite()
downstream.writeToReader("abc")
upstream.writeToReader("abc")
@@ -42,7 +42,7 @@ func TestPipeBidirectionalClosingWriteBothSidesAlsoExists(t *testing.T) {
func TestPipeBidirectionalClosingWriteSingleSideAlsoExists(t *testing.T) {
fun := func(upstream, downstream *mockedStream) {
_ = downstream.CloseWrite()
downstream.CloseWrite()
downstream.writeToReader("abc")
upstream.writeToReader("abc")
@@ -51,46 +51,6 @@ func TestPipeBidirectionalClosingWriteSingleSideAlsoExists(t *testing.T) {
testPipeBidirectionalUnblocking(t, fun, time.Millisecond*200, true)
}
// TestPipeBidirectionalReturnsWhenBothSidesFinish verifies that
// PipeBidirectional returns as soon as both stream directions finish, without
// waiting for the full timeout grace period to expire. This guards against a
// regression where the second-stream wait would block for the whole timeout
// even when the result is already available.
func TestPipeBidirectionalReturnsWhenBothSidesFinish(t *testing.T) {
t.Parallel()
const (
timeout = time.Second * 5
maxWallTime = time.Millisecond * 500
)
logger := zerolog.Nop()
downstream := newMockedStream()
upstream := newMockedStream()
resultCh := make(chan error, 1)
go func() {
resultCh <- PipeBidirectional(downstream, upstream, timeout, &logger)
}()
// Close both reader sides so both stream directions reach EOF promptly.
downstream.closeReader()
upstream.closeReader()
start := time.Now()
select {
case err := <-resultCh:
elapsed := time.Since(start)
require.NoError(t, err)
require.Less(t, elapsed, maxWallTime,
"PipeBidirectional should return as soon as both streams finish, not after the full %s timeout (took %s)",
timeout, elapsed,
)
case <-time.After(timeout):
require.Fail(t, "PipeBidirectional did not return before the timeout expired")
}
}
func testPipeBidirectionalUnblocking(t *testing.T, afterFun func(*mockedStream, *mockedStream), timeout time.Duration, expectTimeout bool) {
logger := zerolog.Nop()
@@ -107,9 +67,9 @@ func testPipeBidirectionalUnblocking(t *testing.T, afterFun func(*mockedStream,
select {
case err := <-resultCh:
if expectTimeout {
require.Error(t, err)
require.NotNil(t, err)
} else {
require.NoError(t, err)
require.Nil(t, err)
}
case <-time.After(timeout * 2):