fix(handler/forward): split readTimeout from pipe idleTimeout to prevent 15s download abort
The readTimeout field (default 15s) was being applied via xnet.Pipe to both directions of a bidirectional proxy connection. During asymmetric transfers (e.g. HTTP file download), the direction reading from the tunnel sees no data after the initial request is forwarded, causing SetReadDeadline to fire after 15s and abort the entire transfer. Fix: add a separate idleTimeout field (default 0=disabled) to the metadata structs in both forward/local and forward/remote handlers, and switch xnet.Pipe to use idleTimeout instead of readTimeout. The readTimeout field now only applies to the initial protocol sniffing/handshake phase. Also document readTimeout vs idleTimeout semantics across all 24 locations in the x/ module where these timeouts appear: - readTimeout: handshake sniffing deadline (handlers), upstream response header timeout (http.Transport), or transport-level read deadline - idleTimeout: idle read deadline per Pipe direction (0=disabled) - ReadTimeout on Sniffer/SnifferBuilder: upstream response header/TLS handshake read timeout during sniffing
This commit is contained in:
@@ -18,6 +18,11 @@ const (
|
|||||||
|
|
||||||
// metadata holds parsed DNS handler configuration.
|
// metadata holds parsed DNS handler configuration.
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for reading DNS query and writing DNS
|
||||||
|
// response on the client connection. Each DNS exchange (one query +
|
||||||
|
// one response) must complete within this window.
|
||||||
|
// Default: 0 (no timeout set by handler — DNS upstream timeout is
|
||||||
|
// controlled separately by the "timeout" field).
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
ttl time.Duration
|
ttl time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (h *forwardHandler) handleRawForwarding(ctx context.Context, conn net.Conn,
|
|||||||
|
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
|
log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
|
||||||
if err := xnet.Pipe(ctx, conn, cc, xnet.WithReadTimeout(h.md.readTimeout)); err != nil {
|
if err := xnet.Pipe(ctx, conn, cc, xnet.WithReadTimeout(h.md.idleTimeout)); err != nil {
|
||||||
log.Debugf("pipe: %v", err)
|
log.Debugf("pipe: %v", err)
|
||||||
}
|
}
|
||||||
log.WithFields(map[string]any{
|
log.WithFields(map[string]any{
|
||||||
|
|||||||
@@ -13,7 +13,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for the initial protocol sniffing phase
|
||||||
|
// (used by SnifferBuilder.ReadTimeout). After sniffing completes and
|
||||||
|
// raw forwarding begins, this timeout no longer applies.
|
||||||
|
// Default: 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
|
// idleTimeout is the idle read timeout applied to each direction of
|
||||||
|
// the bidirectional pipe (xnet.Pipe). A value of 0 or negative disables
|
||||||
|
// the timeout entirely, relying on TCP keepalives and context
|
||||||
|
// cancellation to detect dead connections.
|
||||||
|
// Default: 0 (disabled).
|
||||||
|
idleTimeout time.Duration
|
||||||
httpKeepalive bool
|
httpKeepalive bool
|
||||||
proxyProtocol int
|
proxyProtocol int
|
||||||
|
|
||||||
@@ -34,6 +44,11 @@ func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
|||||||
h.md.readTimeout = 15 * time.Second
|
h.md.readTimeout = 15 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.md.idleTimeout = mdutil.GetDuration(md, "idleTimeout")
|
||||||
|
if h.md.idleTimeout < 0 {
|
||||||
|
h.md.idleTimeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
h.md.httpKeepalive = mdutil.GetBool(md, "http.keepalive")
|
h.md.httpKeepalive = mdutil.GetBool(md, "http.keepalive")
|
||||||
h.md.proxyProtocol = mdutil.GetInt(md, "proxyProtocol")
|
h.md.proxyProtocol = mdutil.GetInt(md, "proxyProtocol")
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ type SnifferBuilder struct {
|
|||||||
ALPN string
|
ALPN string
|
||||||
CertPool tls_util.CertPool
|
CertPool tls_util.CertPool
|
||||||
MitmBypass bypass.Bypass
|
MitmBypass bypass.Bypass
|
||||||
|
// ReadTimeout is the timeout for reading upstream HTTP response headers
|
||||||
|
// and TLS ServerHello during sniffing. Passed through to forwarder.Sniffer.
|
||||||
|
// See forwarder.Sniffer.ReadTimeout for details.
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (h *forwardHandler) handleRawForwarding(ctx context.Context, conn net.Conn,
|
|||||||
|
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
|
log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
|
||||||
if err := xnet.Pipe(ctx, conn, cc, xnet.WithReadTimeout(h.md.readTimeout)); err != nil {
|
if err := xnet.Pipe(ctx, conn, cc, xnet.WithReadTimeout(h.md.idleTimeout)); err != nil {
|
||||||
log.Debugf("pipe: %v", err)
|
log.Debugf("pipe: %v", err)
|
||||||
}
|
}
|
||||||
log.WithFields(map[string]any{
|
log.WithFields(map[string]any{
|
||||||
|
|||||||
@@ -13,7 +13,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for the initial protocol sniffing phase
|
||||||
|
// (used by SnifferBuilder.ReadTimeout). After sniffing completes and
|
||||||
|
// raw forwarding begins, this timeout no longer applies.
|
||||||
|
// Default: 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
|
// idleTimeout is the idle read timeout applied to each direction of
|
||||||
|
// the bidirectional pipe (xnet.Pipe). A value of 0 or negative disables
|
||||||
|
// the timeout entirely, relying on TCP keepalives and context
|
||||||
|
// cancellation to detect dead connections.
|
||||||
|
// Default: 0 (disabled).
|
||||||
|
idleTimeout time.Duration
|
||||||
httpKeepalive bool
|
httpKeepalive bool
|
||||||
proxyProtocol int
|
proxyProtocol int
|
||||||
|
|
||||||
@@ -34,6 +44,11 @@ func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
|||||||
h.md.readTimeout = 15 * time.Second
|
h.md.readTimeout = 15 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.md.idleTimeout = mdutil.GetDuration(md, "idleTimeout")
|
||||||
|
if h.md.idleTimeout < 0 {
|
||||||
|
h.md.idleTimeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
h.md.httpKeepalive = mdutil.GetBool(md, "http.keepalive")
|
h.md.httpKeepalive = mdutil.GetBool(md, "http.keepalive")
|
||||||
h.md.proxyProtocol = mdutil.GetInt(md, "proxyProtocol")
|
h.md.proxyProtocol = mdutil.GetInt(md, "proxyProtocol")
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ type SnifferBuilder struct {
|
|||||||
ALPN string
|
ALPN string
|
||||||
CertPool tls_util.CertPool
|
CertPool tls_util.CertPool
|
||||||
MitmBypass bypass.Bypass
|
MitmBypass bypass.Bypass
|
||||||
|
// ReadTimeout is the timeout for reading upstream HTTP response headers
|
||||||
|
// and TLS ServerHello during sniffing. Passed through to forwarder.Sniffer.
|
||||||
|
// See forwarder.Sniffer.ReadTimeout for details.
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,9 @@ type SnifferBuilder struct {
|
|||||||
ALPN string
|
ALPN string
|
||||||
CertPool tls_util.CertPool
|
CertPool tls_util.CertPool
|
||||||
MitmBypass bypass.Bypass
|
MitmBypass bypass.Bypass
|
||||||
|
// ReadTimeout is the timeout for reading upstream HTTP response headers
|
||||||
|
// and TLS ServerHello during sniffing. Passed through to sniffing.Sniffer.
|
||||||
|
// See sniffing.Sniffer.ReadTimeout for details.
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const (
|
|||||||
// Fields are populated by parseMetadata and read throughout the handler's
|
// Fields are populated by parseMetadata and read throughout the handler's
|
||||||
// request-processing methods.
|
// request-processing methods.
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
readTimeout time.Duration // read timeout for upstream responses; default 15s, negative disables
|
readTimeout time.Duration // deadline for upstream response headers (http.Transport.ResponseHeaderTimeout); 0=15s default, negative=disabled
|
||||||
idleTimeout time.Duration // idle timeout for pipe forwarding; 0 means disabled
|
idleTimeout time.Duration // idle read deadline per Pipe direction during CONNECT/forwarding; 0 or negative = disabled
|
||||||
keepalive bool // enable HTTP keep-alive on the upstream transport
|
keepalive bool // enable HTTP keep-alive on the upstream transport
|
||||||
compression bool // enable HTTP compression on the upstream transport
|
compression bool // enable HTTP compression on the upstream transport
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type metadata struct {
|
|||||||
authBasicRealm string
|
authBasicRealm string
|
||||||
observerPeriod time.Duration
|
observerPeriod time.Duration
|
||||||
observerResetTraffic bool
|
observerResetTraffic bool
|
||||||
idleTimeout time.Duration
|
idleTimeout time.Duration // idle read deadline per Pipe direction during forwarding; read from "readTimeout" or "read.timeout" metadata key. 0 = disabled.
|
||||||
|
|
||||||
limiterRefreshInterval time.Duration
|
limiterRefreshInterval time.Duration
|
||||||
limiterCleanupInterval time.Duration
|
limiterCleanupInterval time.Duration
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is passed to SnifferBuilder as the timeout for reading
|
||||||
|
// upstream response headers during HTTP/TLS sniffing. It is NOT used
|
||||||
|
// as a deadline on the initial client connection (unlike socks/ss
|
||||||
|
// handlers), because redirect/tcp has no protocol handshake.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
tproxy bool
|
tproxy bool
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for reading the initial relay protocol
|
||||||
|
// handshake (relay.Request) from the client connection. The deadline
|
||||||
|
// is cleared immediately after the handshake completes, so it does
|
||||||
|
// not affect subsequent data transfer. Also passed to SnifferBuilder
|
||||||
|
// for the upstream response header read timeout.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
udpBufferSize int
|
udpBufferSize int
|
||||||
enableBind bool
|
enableBind bool
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for reading the initial relay protocol
|
||||||
|
// handshake from the client connection. The deadline is cleared
|
||||||
|
// after the handshake, so it does not affect subsequent data
|
||||||
|
// transfer.
|
||||||
|
// 0 or negative means no timeout is applied.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
bufferSize int
|
bufferSize int
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is passed to SnifferBuilder as the timeout for reading
|
||||||
|
// upstream response headers during HTTP/TLS sniffing. It is NOT used
|
||||||
|
// as a deadline on the initial client connection (unlike socks/ss
|
||||||
|
// handlers), because SNI routing has no protocol handshake beyond
|
||||||
|
// the TLS ClientHello which is parsed during sniffing.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
hash string
|
hash string
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for reading the initial SOCKS4/4A
|
||||||
|
// handshake (connect/bind request) from the client connection. The
|
||||||
|
// deadline is cleared after the handshake, so it does not affect
|
||||||
|
// subsequent data transfer. Also passed to SnifferBuilder for the
|
||||||
|
// upstream response header read timeout.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
hash string
|
hash string
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import (
|
|||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
publicAddr string
|
publicAddr string
|
||||||
|
// readTimeout is the deadline for reading the initial SOCKS5
|
||||||
|
// handshake (auth + connect/associate/udp request) from the client
|
||||||
|
// connection. The deadline is cleared after the handshake, so it
|
||||||
|
// does not affect subsequent data transfer. Also passed to
|
||||||
|
// SnifferBuilder for the upstream response header read timeout.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
noTLS bool
|
noTLS bool
|
||||||
enableBind bool
|
enableBind bool
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ import (
|
|||||||
type metadata struct {
|
type metadata struct {
|
||||||
key string
|
key string
|
||||||
hash string
|
hash string
|
||||||
|
// readTimeout is the deadline for reading the initial Shadowsocks
|
||||||
|
// handshake from the client connection. The deadline is cleared
|
||||||
|
// after the handshake, so it does not affect subsequent data
|
||||||
|
// transfer. Also passed to SnifferBuilder for the upstream response
|
||||||
|
// header read timeout.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
|
|
||||||
sniffing bool
|
sniffing bool
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ const (
|
|||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
key string
|
key string
|
||||||
|
// readTimeout is the deadline for reading a single UDP datagram from
|
||||||
|
// the client. Since UDP is connectionless, this timeout applies to
|
||||||
|
// each individual datagram read. Default: 1 minute (longer than the
|
||||||
|
// typical 15s used by TCP handlers to accommodate the stateless
|
||||||
|
// nature of UDP).
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
udpBufferSize int
|
udpBufferSize int
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for reading the initial SSH handshake
|
||||||
|
// from the client connection. The deadline is cleared after the
|
||||||
|
// handshake, so it does not affect subsequent data transfer. Also
|
||||||
|
// passed to SnifferBuilder for the upstream response header read
|
||||||
|
// timeout.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
|
|
||||||
sniffing bool
|
sniffing bool
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ type entrypoint struct {
|
|||||||
sniffingWebsocket bool
|
sniffingWebsocket bool
|
||||||
websocketSampleRate float64
|
websocketSampleRate float64
|
||||||
|
|
||||||
|
// readTimeout is applied as SetReadDeadline on the upstream connection
|
||||||
|
// before sniffing HTTP/TLS reads. It mirrors entryPointReadTimeout
|
||||||
|
// from the handler metadata.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for reading the initial relay protocol
|
||||||
|
// handshake from the client connection. The deadline is cleared
|
||||||
|
// after the handshake, so it does not affect subsequent data
|
||||||
|
// transfer. 0 or negative means no timeout is applied.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
|
|
||||||
entrypoints []entrypointConfig
|
entrypoints []entrypointConfig
|
||||||
@@ -30,7 +34,7 @@ type metadata struct {
|
|||||||
entryPointProxyProtocol int
|
entryPointProxyProtocol int
|
||||||
entryPointKeepalive bool
|
entryPointKeepalive bool
|
||||||
entryPointCompression bool
|
entryPointCompression bool
|
||||||
entryPointReadTimeout time.Duration
|
entryPointReadTimeout time.Duration // deadline for reading upstream HTTP response headers in the entrypoint's http.Transport.ResponseHeaderTimeout. Also passed as readTimeout to entrypoint dialer for SetReadDeadline on upstream conn. 0 or negative defaults to 15s.
|
||||||
sniffingWebsocket bool
|
sniffingWebsocket bool
|
||||||
sniffingWebsocketSampleRate float64
|
sniffingWebsocketSampleRate float64
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
|
// readTimeout is the deadline for reading data from the upstream
|
||||||
|
// (the target Unix socket or pipe). It is set on the upstream conn
|
||||||
|
// via SetReadDeadline before each sniffing HTTP/TLS read. Also
|
||||||
|
// passed to SnifferBuilder for the upstream response header read
|
||||||
|
// timeout.
|
||||||
|
// 0 or negative defaults to 15s.
|
||||||
readTimeout time.Duration
|
readTimeout time.Duration
|
||||||
|
|
||||||
sniffing bool
|
sniffing bool
|
||||||
|
|||||||
@@ -122,6 +122,13 @@ type Sniffer struct {
|
|||||||
CertPool tls_util.CertPool
|
CertPool tls_util.CertPool
|
||||||
MitmBypass bypass.Bypass
|
MitmBypass bypass.Bypass
|
||||||
|
|
||||||
|
// ReadTimeout is the deadline for reading the upstream response
|
||||||
|
// headers during HTTP sniffing (http.ReadResponse) and the TLS
|
||||||
|
// ServerHello during TLS sniffing. This timeout is applied once
|
||||||
|
// per request/response pair in the HTTP keep-alive loop and cleared
|
||||||
|
// after each response is received. It does NOT affect the client
|
||||||
|
// connection or the response body transfer.
|
||||||
|
// Default: DefaultReadTimeout (30s) if not set.
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ type Sniffer struct {
|
|||||||
CertPool tls_util.CertPool
|
CertPool tls_util.CertPool
|
||||||
MitmBypass bypass.Bypass
|
MitmBypass bypass.Bypass
|
||||||
|
|
||||||
|
// ReadTimeout is the deadline for reading the upstream response
|
||||||
|
// headers during HTTP sniffing (http.ReadResponse) and the TLS
|
||||||
|
// ServerHello during TLS sniffing. This timeout is applied once
|
||||||
|
// per request/response pair in the HTTP keep-alive loop and cleared
|
||||||
|
// after each response is received. It does NOT affect the client
|
||||||
|
// connection or the response body transfer.
|
||||||
|
// Default: DefaultReadTimeout (30s) if not set.
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user