Make pipe read timeout configurable via idleTimeout metadata

The 30s hardcoded readTimeout in Pipe() caused all CONNECT tunnel
connections to be hard-closed after 30s of inactivity, breaking
WebSocket and long-polling connections through the proxy.

Pipe() now accepts a WithReadTimeout(d) option. When d is 0 (the
default) no read deadline is set, relying on TCP keepalives or context
cancellation to detect dead connections instead.

The HTTP handler exposes this as the idleTimeout metadata key.

Fixes: https://github.com/go-gost/x/issues/91
This commit is contained in:
Andrew Beresford
2026-05-08 11:31:53 +01:00
parent 07ca57055a
commit 290e9c37d5
3 changed files with 40 additions and 12 deletions
+31 -9
View File
@@ -12,11 +12,32 @@ import (
const (
// tcpWaitTimeout implements a TCP half-close timeout.
tcpWaitTimeout = 10 * time.Second
readTimeout = 30 * time.Second
)
// PipeOption configures the behaviour of Pipe.
type PipeOption func(*pipeOptions)
type pipeOptions struct {
readTimeout time.Duration
}
// WithReadTimeout sets an idle read timeout on each pipe half. When a read
// blocks for longer than d the connection is closed. A value of 0 disables
// the timeout entirely, relying on TCP keepalives or context cancellation to
// detect dead connections.
func WithReadTimeout(d time.Duration) PipeOption {
return func(o *pipeOptions) {
o.readTimeout = d
}
}
// Pipe 在两个连接之间建立双向数据通道
func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser) error {
func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser, opts ...PipeOption) error {
var options pipeOptions
for _, opt := range opts {
opt(&options)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -24,11 +45,11 @@ func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser) error {
// 启动两个方向的传输
go func() {
errCh <- pipeHalf(ctx, rw1, rw2)
errCh <- pipeHalf(ctx, rw1, rw2, options.readTimeout)
}()
go func() {
errCh <- pipeHalf(ctx, rw2, rw1)
errCh <- pipeHalf(ctx, rw2, rw1, options.readTimeout)
}()
// 等待第一个错误或完成
@@ -51,7 +72,7 @@ func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser) error {
}
// pipeHalf 单向管道传输
func pipeHalf(ctx context.Context, src, dst io.ReadWriteCloser) error {
func pipeHalf(ctx context.Context, src, dst io.ReadWriteCloser, readTimeout time.Duration) error {
defer func() {
// 传输完成后执行TCP半关闭
halfClose(src, dst)
@@ -60,7 +81,6 @@ func pipeHalf(ctx context.Context, src, dst io.ReadWriteCloser) error {
buf := bufpool.Get(bufferSize / 2)
defer bufpool.Put(buf)
// 创建带超时的读取器
reader := &readDeadliner{
Reader: src,
@@ -73,9 +93,11 @@ func pipeHalf(ctx context.Context, src, dst io.ReadWriteCloser) error {
case <-ctx.Done():
return ctx.Err()
default:
// 设置读取超时
if rd, ok := src.(interface{ SetReadDeadline(time.Time) error }); ok {
rd.SetReadDeadline(time.Now().Add(readTimeout))
// 设置读取超时 (仅当配置了超时时)
if readTimeout > 0 {
if rd, ok := src.(interface{ SetReadDeadline(time.Time) error }); ok {
rd.SetReadDeadline(time.Now().Add(readTimeout))
}
}
// 读取数据