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:
+21
-11
@@ -2,6 +2,7 @@ package stream
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
@@ -9,12 +10,17 @@ 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
|
||||
@@ -37,7 +43,7 @@ type nopCloseWriterAdapter struct {
|
||||
io.ReadWriter
|
||||
}
|
||||
|
||||
func NopCloseWriterAdapter(stream io.ReadWriter) *nopCloseWriterAdapter {
|
||||
func noopCloseWriter(stream io.ReadWriter) *nopCloseWriterAdapter {
|
||||
return &nopCloseWriterAdapter{stream}
|
||||
}
|
||||
|
||||
@@ -72,7 +78,7 @@ func (s *bidirectionalStreamStatus) wait(maxWaitForSecondStream time.Duration) e
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("timeout waiting for second stream to finish")
|
||||
return fmt.Errorf("timeout waiting for second stream to finish %s", maxWaitForSecondStream)
|
||||
case <-s.doneChan:
|
||||
return nil
|
||||
}
|
||||
@@ -85,15 +91,19 @@ func (s *bidirectionalStreamStatus) isAnyDone() bool {
|
||||
}
|
||||
|
||||
// Pipe copies copy data to & from provided io.ReadWriters.
|
||||
func Pipe(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
|
||||
_ = PipeBidirectional(NopCloseWriterAdapter(tunnelConn), NopCloseWriterAdapter(originConn), 0, log)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.Duration, log *zerolog.Logger) error {
|
||||
status := newBiStreamStatus()
|
||||
|
||||
@@ -101,7 +111,7 @@ func PipeBidirectional(downstream, upstream Stream, maxWaitForSecondStream time.
|
||||
go unidirectionalStream(upstream, downstream, "downstream->upstream", status, log)
|
||||
|
||||
if err := status.wait(maxWaitForSecondStream); err != nil {
|
||||
return errors.Wrap(err, "unable to wait for both streams while proxying")
|
||||
return fmt.Errorf("unable to wait for both streams while proxying: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user