From 0ff0f10ac8afa5faf19f8aa9cdaf6ecb4a39b1ef Mon Sep 17 00:00:00 2001 From: SurefireStudios <123013554+SurefireStudios@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:38:33 -0700 Subject: [PATCH] Add --pidfile to cloudflared access tcp Closes #723. The forwarder modes had no way to report their PID, so scripts that background `cloudflared access tcp --url ...` had to grep pgrep output to stop it again. --pidfile writes the PID once the listener host is resolved, so a supervising script can use `pkill -F`. Mirrors the existing --pidfile on `cloudflared tunnel`, including the TUNNEL_PIDFILE environment variable and the same homedir expansion and log-and-continue error handling. --- cmd/cloudflared/access/carrier.go | 24 +++++++++++++ cmd/cloudflared/access/carrier_test.go | 50 ++++++++++++++++++++++++++ cmd/cloudflared/access/cmd.go | 6 ++++ 3 files changed, 80 insertions(+) create mode 100644 cmd/cloudflared/access/carrier_test.go diff --git a/cmd/cloudflared/access/carrier.go b/cmd/cloudflared/access/carrier.go index 52aacc5655f..acd706b6207 100644 --- a/cmd/cloudflared/access/carrier.go +++ b/cmd/cloudflared/access/carrier.go @@ -5,8 +5,11 @@ import ( "fmt" "io" "net/http" + "os" + "path/filepath" "strings" + "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/urfave/cli/v2" @@ -125,6 +128,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 path := c.String(sshPidfileFlag); path != "" { + writePidFile(path, log) + } err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options) if err != nil { log.Err(err).Msg("Error on Websocket listener") @@ -145,3 +151,21 @@ func ssh(c *cli.Context) error { } return carrier.StartClient(wsConn, s, options) } + +// writePidFile writes the current PID to pidPathname so a supervising script can +// stop this forwarder with something like `pkill -F`. Failures are logged rather +// than returned: the forwarder itself is still usable without the file. +func writePidFile(pidPathname string, log *zerolog.Logger) { + expandedPath, err := homedir.Expand(pidPathname) + if err != nil { + log.Err(err).Str("pidPathname", pidPathname).Msg("Unable to expand the path, try to use absolute path in --pidfile") + return + } + file, err := os.Create(filepath.Clean(expandedPath)) + if err != nil { + log.Err(err).Str("expandedPath", expandedPath).Msg("Unable to write pid") + return + } + defer func() { _ = file.Close() }() + _, _ = fmt.Fprintf(file, "%d", os.Getpid()) +} diff --git a/cmd/cloudflared/access/carrier_test.go b/cmd/cloudflared/access/carrier_test.go new file mode 100644 index 00000000000..3945a9239e3 --- /dev/null +++ b/cmd/cloudflared/access/carrier_test.go @@ -0,0 +1,50 @@ +package access + +import ( + "io" + "os" + "path/filepath" + "strconv" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWritePidFile(t *testing.T) { + log := zerolog.New(io.Discard) + path := filepath.Join(t.TempDir(), "cloudflared.pid") + + writePidFile(path, &log) + + contents, err := os.ReadFile(path) + require.NoError(t, err) + + pid, err := strconv.Atoi(string(contents)) + require.NoError(t, err) + assert.Equal(t, os.Getpid(), pid) +} + +func TestWritePidFileTruncatesExistingFile(t *testing.T) { + log := zerolog.New(io.Discard) + path := filepath.Join(t.TempDir(), "cloudflared.pid") + + // a stale file from a previous run must not leave trailing digits behind + require.NoError(t, os.WriteFile(path, []byte("999999999999"), 0o600)) + + writePidFile(path, &log) + + contents, err := os.ReadFile(path) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(os.Getpid()), string(contents)) +} + +func TestWritePidFileUnwritablePathDoesNotPanic(t *testing.T) { + log := zerolog.New(io.Discard) + // a directory that does not exist, so os.Create fails + path := filepath.Join(t.TempDir(), "missing", "cloudflared.pid") + + assert.NotPanics(t, func() { writePidFile(path, &log) }) + assert.NoFileExists(t, path) +} diff --git a/cmd/cloudflared/access/cmd.go b/cmd/cloudflared/access/cmd.go index 636b9288e27..5f5ff030a9e 100644 --- a/cmd/cloudflared/access/cmd.go +++ b/cmd/cloudflared/access/cmd.go @@ -38,6 +38,7 @@ const ( sshGenCertFlag = "short-lived-cert" sshConnectTo = "connect-to" sshDebugStream = "debug-stream" + sshPidfileFlag = "pidfile" sshConfigTemplate = ` Add to your {{.Home}}/.ssh/config: @@ -181,6 +182,11 @@ func Commands() []*cli.Command { Usage: "specify an Access service token secret you wish to use.", EnvVars: []string{"TUNNEL_SERVICE_TOKEN_SECRET"}, }, + &cli.StringFlag{ + Name: sshPidfileFlag, + Usage: "Write the application's PID to this file once the listener is ready.", + EnvVars: []string{"TUNNEL_PIDFILE"}, + }, &cli.StringFlag{ Name: cfdflags.LogFile, Usage: "Save application log to this file for reporting issues.",