mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-08-07 07:14:57 +00:00
TUN-10621: Propagate max wait timeout
This PR addresses an issue where cloudflared prematurely closes the origin connection before the upstream-to-downstream goroutine finishes reading, causing intermittent connection drops when a client immediately closes the write-side of a connection. When a client finishes writing data, it immediately closes its side of the connection. Under the current implementation in cloudflared's downstream-to-upstream goroutine does not wait for the second stream to complete. It unblocks the moment the first stream writes to the channel. Once this happens, the pipe returns control to `proxyTCPStream`, which prematurely closes the origin connection. Consequently, when the upstream-to-downstream goroutine attempts to read the remaining data from the origin connection, the connection is already gone, leading to unexpected failures. We started propagating the `TimeoutAfterFirstClose` configuration/parameter. This allows the proxy to wait for a designated period, giving the second stream sufficient time to finish processing and read all remaining data before `proxyTCPStream` tears down the origin connection.
This commit is contained in:
+45
-5
@@ -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,6 +51,46 @@ 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()
|
||||
|
||||
@@ -67,9 +107,9 @@ func testPipeBidirectionalUnblocking(t *testing.T, afterFun func(*mockedStream,
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
if expectTimeout {
|
||||
require.NotNil(t, err)
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
case <-time.After(timeout * 2):
|
||||
|
||||
Reference in New Issue
Block a user