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:
Miguel da Costa Martins Marcelino
2026-06-29 10:57:12 +00:00
parent dba2d33a6b
commit 5c66bd68ab
9 changed files with 93 additions and 39 deletions
+10 -8
View File
@@ -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,6 +756,8 @@ 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,
@@ -801,8 +803,8 @@ func (p *pipedRequestBody) roundtrip(addr string) []byte {
if err != nil {
panic(err)
}
defer conn.Close()
defer resp.Body.Close()
defer func() { _ = conn.Close() }()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusSwitchingProtocols {
panic(fmt.Errorf("resp returned status code: %d", resp.StatusCode))
@@ -949,7 +951,7 @@ func runEchoTCPService(t *testing.T, l net.Listener) {
if err != nil {
panic(err)
}
defer conn.Close()
defer func() { _ = conn.Close() }()
for {
buf := make([]byte, 1024)
@@ -987,7 +989,7 @@ func runEchoWSService(t *testing.T, l net.Listener) {
t.Log(err)
return
}
defer conn.Close()
defer func() { _ = conn.Close() }()
for {
messageType, p, err := conn.ReadMessage()