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.
|
||||
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
|
||||
ttl time.Duration
|
||||
timeout time.Duration
|
||||
|
||||
@@ -82,7 +82,7 @@ func (h *forwardHandler) handleRawForwarding(ctx context.Context, conn net.Conn,
|
||||
|
||||
t := time.Now()
|
||||
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.WithFields(map[string]any{
|
||||
|
||||
@@ -13,7 +13,17 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
// 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
|
||||
proxyProtocol int
|
||||
|
||||
@@ -34,6 +44,11 @@ func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
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.proxyProtocol = mdutil.GetInt(md, "proxyProtocol")
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ type SnifferBuilder struct {
|
||||
ALPN string
|
||||
CertPool tls_util.CertPool
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func (h *forwardHandler) handleRawForwarding(ctx context.Context, conn net.Conn,
|
||||
|
||||
t := time.Now()
|
||||
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.WithFields(map[string]any{
|
||||
|
||||
@@ -13,7 +13,17 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
// 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
|
||||
proxyProtocol int
|
||||
|
||||
@@ -34,6 +44,11 @@ func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
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.proxyProtocol = mdutil.GetInt(md, "proxyProtocol")
|
||||
|
||||
|
||||
@@ -32,6 +32,9 @@ type SnifferBuilder struct {
|
||||
ALPN string
|
||||
CertPool tls_util.CertPool
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,9 @@ type SnifferBuilder struct {
|
||||
ALPN string
|
||||
CertPool tls_util.CertPool
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ const (
|
||||
// Fields are populated by parseMetadata and read throughout the handler's
|
||||
// request-processing methods.
|
||||
type metadata struct {
|
||||
readTimeout time.Duration // read timeout for upstream responses; default 15s, negative disables
|
||||
idleTimeout time.Duration // idle timeout for pipe forwarding; 0 means disabled
|
||||
readTimeout time.Duration // deadline for upstream response headers (http.Transport.ResponseHeaderTimeout); 0=15s default, negative=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
|
||||
compression bool // enable HTTP compression on the upstream transport
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ type metadata struct {
|
||||
authBasicRealm string
|
||||
observerPeriod time.Duration
|
||||
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
|
||||
limiterCleanupInterval time.Duration
|
||||
|
||||
@@ -13,6 +13,11 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
tproxy bool
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
udpBufferSize int
|
||||
enableBind bool
|
||||
|
||||
@@ -18,6 +18,11 @@ const (
|
||||
)
|
||||
|
||||
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
|
||||
bufferSize int
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
hash string
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
hash string
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ import (
|
||||
|
||||
type metadata struct {
|
||||
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
|
||||
noTLS bool
|
||||
enableBind bool
|
||||
|
||||
@@ -16,6 +16,12 @@ import (
|
||||
type metadata struct {
|
||||
key 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
|
||||
|
||||
sniffing bool
|
||||
|
||||
@@ -14,6 +14,11 @@ const (
|
||||
|
||||
type metadata struct {
|
||||
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
|
||||
udpBufferSize int
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
sniffing bool
|
||||
|
||||
@@ -63,6 +63,9 @@ type entrypoint struct {
|
||||
sniffingWebsocket bool
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ const (
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
entrypoints []entrypointConfig
|
||||
@@ -30,7 +34,7 @@ type metadata struct {
|
||||
entryPointProxyProtocol int
|
||||
entryPointKeepalive 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
|
||||
sniffingWebsocketSampleRate float64
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
sniffing bool
|
||||
|
||||
@@ -122,6 +122,13 @@ type Sniffer struct {
|
||||
CertPool tls_util.CertPool
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,13 @@ type Sniffer struct {
|
||||
CertPool tls_util.CertPool
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user