mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-10-12 02:12:14 +00:00
TUN-10621: Test websocket path
Adding a test that tries to exercise the race condition for the websocket path. The sequence is as follows: 1. The test starts by writing the payload to the mock server 2. Then, it closes the write side of the connection. 3. It waits for 3 seconds to make the race window explicit: the bug in cloudflared would have closed the connection and we would not be able to read. 4. Read the message. With the fix applied, we should be able to read the full message.
This commit is contained in:
committed by
João "Pisco" Fernandes
parent
a9d023b7a2
commit
c06b2d43e9
@@ -5,8 +5,12 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -17,6 +21,7 @@ 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"
|
||||
)
|
||||
@@ -122,3 +127,137 @@ 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
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -299,5 +299,5 @@ func wasInstalledFromPackageManager() bool {
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
return term.IsTerminal(int(os.Stdout.Fd())) //nolint:gosec
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user