mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 15:24:46 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8679787525 | |||
| 45c0f22e97 | |||
| e8956c10ac | |||
| 541a608dde | |||
| 6fd720a4a4 | |||
| 60a8c3da17 | |||
| 2601f87b57 | |||
| f70adda11c | |||
| ecb88678f1 | |||
| 43bfec0bcd |
@@ -3,23 +3,12 @@
|
||||
runOnBranches: "^master$"
|
||||
COMPONENT: "common"
|
||||
|
||||
.register_inputs_stable_bookworm: ®ister_inputs_stable_bookworm
|
||||
<<: *register_inputs
|
||||
runOnChangesTo: ['RELEASE_NOTES']
|
||||
FLAVOR: "bookworm"
|
||||
SERIES: "stable"
|
||||
|
||||
.register_inputs_stable_trixie: ®ister_inputs_stable_trixie
|
||||
<<: *register_inputs
|
||||
runOnChangesTo: ['RELEASE_NOTES']
|
||||
FLAVOR: "trixie"
|
||||
SERIES: "stable"
|
||||
|
||||
.register_inputs_next_bookworm: ®ister_inputs_next_bookworm
|
||||
<<: *register_inputs
|
||||
FLAVOR: "bookworm"
|
||||
SERIES: next
|
||||
|
||||
.register_inputs_next_trixie: ®ister_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,5 +1,5 @@
|
||||
images:
|
||||
- name: cloudflared-daemon
|
||||
- name: cloudflared
|
||||
dockerfile: Dockerfile.$ARCH
|
||||
context: .
|
||||
version_file: versions-internal
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user