From 79ebeb3f1f837ff21f19e215cf3705122270d8d8 Mon Sep 17 00:00:00 2001 From: Mauro Druwel Date: Mon, 12 Oct 2026 06:29:22 +0000 Subject: [PATCH] feat(access): add --pidfile support for access command (fixes #723) --- cmd/cloudflared/access/carrier.go | 8 ++++ cmd/cloudflared/access/cmd.go | 20 ++++++-- cmd/cloudflared/access/pidfile_test.go | 56 +++++++++++++++++++++++ cmd/cloudflared/cliutil/pidfile.go | 33 +++++++++++++ cmd/cloudflared/cliutil/pidfile_test.go | 61 +++++++++++++++++++++++++ cmd/cloudflared/flags/flags.go | 3 ++ cmd/cloudflared/tunnel/cmd.go | 15 +----- 7 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 cmd/cloudflared/access/pidfile_test.go create mode 100644 cmd/cloudflared/cliutil/pidfile.go create mode 100644 cmd/cloudflared/cliutil/pidfile_test.go diff --git a/cmd/cloudflared/access/carrier.go b/cmd/cloudflared/access/carrier.go index 52aacc5655f..202e6b16e2d 100644 --- a/cmd/cloudflared/access/carrier.go +++ b/cmd/cloudflared/access/carrier.go @@ -12,6 +12,8 @@ import ( "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/carrier" + "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" + cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/stream" @@ -125,6 +127,9 @@ func ssh(c *cli.Context) error { return errors.Wrap(err, "error validating origin URL") } log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener") + if pidfile := c.String(cfdflags.Pidfile); pidfile != "" { + _ = cliutil.WritePidFile(pidfile, log) + } err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options) if err != nil { log.Err(err).Msg("Error on Websocket listener") @@ -143,5 +148,8 @@ func ssh(c *cli.Context) error { logger := log.With().Str("host", url.Host).Logger() s = stream.NewDebugStream(s, &logger, maxMessages) } + if pidfile := c.String(cfdflags.Pidfile); pidfile != "" { + _ = cliutil.WritePidFile(pidfile, log) + } return carrier.StartClient(wsConn, s, options) } diff --git a/cmd/cloudflared/access/cmd.go b/cmd/cloudflared/access/cmd.go index 636b9288e27..f5440ad49cf 100644 --- a/cmd/cloudflared/access/cmd.go +++ b/cmd/cloudflared/access/cmd.go @@ -80,10 +80,17 @@ func Commands() []*cli.Command { Aliases: []string{"forward"}, Category: "Access", Usage: "access ", - Flags: []cli.Flag{&cli.BoolFlag{ - Name: fedrampFlag, - Usage: "use when performing operations in fedramp account", - }}, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: fedrampFlag, + Usage: "use when performing operations in fedramp account", + }, + &cli.StringFlag{ + Name: cfdflags.Pidfile, + Usage: "Write the application's PID to this file after first successful connection.", + EnvVars: []string{"TUNNEL_PIDFILE"}, + }, + }, Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are able to reach sensitive resources. The commands provided here allow you to interact with Access protected @@ -204,6 +211,11 @@ func Commands() []*cli.Command { Hidden: true, Usage: "Writes up-to the max provided stream payloads to the logger as debug statements.", }, + &cli.StringFlag{ + Name: cfdflags.Pidfile, + Usage: "Write the application's PID to this file after first successful connection.", + EnvVars: []string{"TUNNEL_PIDFILE"}, + }, }, }, { diff --git a/cmd/cloudflared/access/pidfile_test.go b/cmd/cloudflared/access/pidfile_test.go new file mode 100644 index 00000000000..63fff4e46f7 --- /dev/null +++ b/cmd/cloudflared/access/pidfile_test.go @@ -0,0 +1,56 @@ +package access + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" + + cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" +) + +func TestAccessCommandHasPidfileFlag(t *testing.T) { + t.Parallel() + + cmds := Commands() + require.NotEmpty(t, cmds) + + var accessCmd *cli.Command + for _, cmd := range cmds { + if cmd.Name == "access" { + accessCmd = cmd + break + } + } + require.NotNil(t, accessCmd) + + var accessPidFlag *cli.StringFlag + for _, f := range accessCmd.Flags { + if sf, ok := f.(*cli.StringFlag); ok && sf.Name == cfdflags.Pidfile { + accessPidFlag = sf + break + } + } + require.NotNil(t, accessPidFlag, "access command should have --pidfile flag") + assert.Contains(t, accessPidFlag.EnvVars, "TUNNEL_PIDFILE") + + var tcpCmd *cli.Command + for _, sub := range accessCmd.Subcommands { + if sub.Name == "tcp" { + tcpCmd = sub + break + } + } + require.NotNil(t, tcpCmd) + + var tcpPidFlag *cli.StringFlag + for _, f := range tcpCmd.Flags { + if sf, ok := f.(*cli.StringFlag); ok && sf.Name == cfdflags.Pidfile { + tcpPidFlag = sf + break + } + } + require.NotNil(t, tcpPidFlag, "access tcp subcommand should have --pidfile flag") + assert.Contains(t, tcpPidFlag.EnvVars, "TUNNEL_PIDFILE") +} diff --git a/cmd/cloudflared/cliutil/pidfile.go b/cmd/cloudflared/cliutil/pidfile.go new file mode 100644 index 00000000000..37e513cd340 --- /dev/null +++ b/cmd/cloudflared/cliutil/pidfile.go @@ -0,0 +1,33 @@ +package cliutil + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/mitchellh/go-homedir" + "github.com/rs/zerolog" +) + +const ( + LogFieldPIDPathname = "pidPathname" + LogFieldExpandedPath = "expandedPath" +) + +// WritePidFile writes the current process ID into the specified path. +func WritePidFile(pidPathname string, log *zerolog.Logger) error { + expandedPath, err := homedir.Expand(pidPathname) + if err != nil { + log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile") + return err + } + cleanPath := filepath.Clean(expandedPath) + file, err := os.Create(cleanPath) + if err != nil { + log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid") + return err + } + defer func() { _ = file.Close() }() + _, err = fmt.Fprintf(file, "%d", os.Getpid()) + return err +} diff --git a/cmd/cloudflared/cliutil/pidfile_test.go b/cmd/cloudflared/cliutil/pidfile_test.go new file mode 100644 index 00000000000..ab686afa28c --- /dev/null +++ b/cmd/cloudflared/cliutil/pidfile_test.go @@ -0,0 +1,61 @@ +package cliutil + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWritePidFile_Success(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + pidPath := filepath.Join(tempDir, "test.pid") + logger := zerolog.Nop() + + err := WritePidFile(pidPath, &logger) + require.NoError(t, err) + + content, err := os.ReadFile(pidPath) + require.NoError(t, err) + + pid, err := strconv.Atoi(strings.TrimSpace(string(content))) + require.NoError(t, err) + assert.Equal(t, os.Getpid(), pid) +} + +func TestWritePidFile_InvalidPath(t *testing.T) { + t.Parallel() + + // An impossible path where a directory cannot be written into + pidPath := filepath.Join("/nonexistent_dir_12345", "sub", "test.pid") + logger := zerolog.Nop() + + err := WritePidFile(pidPath, &logger) + require.Error(t, err) +} + +func TestWritePidFile_HomedirExpansion(t *testing.T) { + tempHome := t.TempDir() + t.Setenv("HOME", tempHome) + + pidPath := "~/test_homedir.pid" + logger := zerolog.Nop() + + err := WritePidFile(pidPath, &logger) + require.NoError(t, err) + + expectedPath := filepath.Join(tempHome, "test_homedir.pid") + content, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + pid, err := strconv.Atoi(strings.TrimSpace(string(content))) + require.NoError(t, err) + assert.Equal(t, os.Getpid(), pid) +} diff --git a/cmd/cloudflared/flags/flags.go b/cmd/cloudflared/flags/flags.go index a792bc808ce..39e76f892ee 100644 --- a/cmd/cloudflared/flags/flags.go +++ b/cmd/cloudflared/flags/flags.go @@ -175,4 +175,7 @@ const ( // Automatically close the login interstitial browser window after the user makes a decision. AutoCloseInterstitial = "auto-close" + + // Pidfile writes the application's process ID to a file. + Pidfile = "pidfile" ) diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 30a4862d39a..54625807847 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -14,7 +14,6 @@ import ( "github.com/coreos/go-systemd/v22/daemon" "github.com/facebookgo/grace/gracenet" "github.com/getsentry/sentry-go" - "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" @@ -615,19 +614,7 @@ func notifySystemd(waitForSignal *signal.Signal) { func writePidFile(waitForSignal *signal.Signal, pidPathname string, log *zerolog.Logger) { <-waitForSignal.Wait() - expandedPath, err := homedir.Expand(pidPathname) - if err != nil { - log.Err(err).Str(LogFieldPIDPathname, pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile") - return - } - cleanPath := filepath.Clean(expandedPath) - file, err := os.Create(cleanPath) - if err != nil { - log.Err(err).Str(LogFieldExpandedPath, expandedPath).Msg("Unable to write pid") - return - } - defer func() { _ = file.Close() }() - _, _ = fmt.Fprintf(file, "%d", os.Getpid()) + _ = cliutil.WritePidFile(pidPathname, log) } func hostnameFromURI(uri string) string {